From 98b35c8377987b041790b79470e7afc3cb7fa133 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 21 Dec 2023 12:22:39 +0300 Subject: [PATCH 01/81] feat(imports): Allow users to import exported data using new format - Added new import-archives-v2 command - Exported docs now are valid JSON objects - Users will need to update Tangerine client and re-export files Refs Tangerine-Community/Tangerine#3145 --- .../export-data/export-data.component.ts | 3 +- server/package.json | 1 + server/src/scripts/import-archives-v2/bin.js | 55 +++++++++++++++++++ server/src/scripts/info.sh | 3 +- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100755 server/src/scripts/import-archives-v2/bin.js diff --git a/client/src/app/core/export-data/export-data/export-data.component.ts b/client/src/app/core/export-data/export-data/export-data.component.ts index 870ad77e75..94157f4750 100644 --- a/client/src/app/core/export-data/export-data/export-data.component.ts +++ b/client/src/app/core/export-data/export-data/export-data.component.ts @@ -275,11 +275,12 @@ export class ExportDataComponent implements OnInit { const stream = new window['Memorystream'] let data = ''; stream.on('data', function (chunk) { - data += chunk.toString(); + data += chunk.toString() +',';// Add comma after each item - will be useful in creating a valid JSON object }); await db.dump(stream) console.log('Successfully exported : ' + dbName); this.statusMessage += `

${_TRANSLATE('Successfully exported database ')} ${dbName}

` + data = `[${data.replace(/,([^,]*)$/, '$1')}]`//Make a valid JSON string - Wrap the items in [] and remove trailing comma const file = new Blob([data], {type: 'application/json'}); this.downloadData(file, fileName, 'application/json'); this.hideExportButton = false diff --git a/server/package.json b/server/package.json index ca98b895bb..881db6d507 100644 --- a/server/package.json +++ b/server/package.json @@ -29,6 +29,7 @@ "update-group-archived-index": "./src/scripts/update-group-archived-index.js", "find-missing-records": "./src/scripts/find-missing-records.js", "import-archives": "./src/scripts/import-archives/bin.js", + "import-archives-v2": "./src/scripts/import-archives-v2/bin.js", "reset-all-devices": "./src/scripts/reset-all-devices/bin.js", "translations-update": "./src/scripts/translations-update.js", "release-dat": "./src/scripts/release-dat.sh", diff --git a/server/src/scripts/import-archives-v2/bin.js b/server/src/scripts/import-archives-v2/bin.js new file mode 100755 index 0000000000..3f75e2ea8b --- /dev/null +++ b/server/src/scripts/import-archives-v2/bin.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +if (!process.argv[2]) { + console.log('Place archives from clients into the ./data/archives folder on the host machine then run...') + console.log(' ./bin.js ') + process.exit() +} + +const util = require('util') +const readdir = util.promisify(require('fs').readdir) +const readFile = util.promisify(require('fs').readFile) +const pako = require('pako') +const axios = require('axios') +const url = `http://localhost/api/${process.argv[2]}/upload` +const ARCHIVES_PATH = '/archives' + + +async function go() { + const archivesList = await readdir(ARCHIVES_PATH) + for (const archivePath of archivesList) { + const archiveContents = await readFile(`${ARCHIVES_PATH}/${archivePath}`, 'utf-8') + const docsArray = ([...JSON.parse(archiveContents)]).find(item=>item.docs)?.docs + const userProfileDoc = docsArray.find(item=> item.form&& item.form.id=== 'user-profile') + console.log(userProfileDoc) + const docs = docsArray + .map(item => { + if (item.collection !== 'TangyFormResponse') return + if (item.form && item.form.id !== 'user-profile') { + item.items[0].inputs.push({ + name: 'userProfileId', + value: userProfileDoc._id + }) + } + return item + }) + .filter(doc => doc !== undefined) + for (const doc of docs) { + let body = pako.deflate(JSON.stringify({ doc }), {to: 'string'}) + await axios({ + method: 'post', + url, + data: `${body}`, + headers: { + 'content-type': 'text/plain', + 'Authorization': `${process.env.T_UPLOAD_TOKEN}` + } + }) + } + } +} + +try { + go() +} catch(e) { + console.log(e) +} diff --git a/server/src/scripts/info.sh b/server/src/scripts/info.sh index 6ebf79f85a..f36cb3b492 100755 --- a/server/src/scripts/info.sh +++ b/server/src/scripts/info.sh @@ -14,7 +14,8 @@ echo "generate-cases (Generate cases with group's case-export.json as echo "reset-all-devices (Reset server tokens and device keys for all devices, requires reinstall and set up on all devices after)" echo "push-all-groups-views (Push all database views into all groups)" echo "index-all-groups-views (Index all database views into all groups)" -echo "import-archives (Import client archives from the ./data/archives folder)" +echo "import-archives (Import client archives from the ./data/archives folder using old format)" +echo "import-archives-v2 (Import client archives from the ./data/archives folder using new format)" echo "release-apk (Release a Group App as an APK)" echo "release-pwa (Release a Group App as a PWA)" echo "release-dat (Release a Group APP as a Dat Archive)" From 87ab91709e14c1aea65db03deb48baa4e2360f90 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Mon, 22 Jan 2024 15:53:07 +0300 Subject: [PATCH 02/81] fix(editor): When editing uploads, the form responses should be unlocked Add @Input to allow for unlocking of forms when in `data/uploads` Refs Tangerine-Community/Tangerine#3681 --- .../groups/group-uploads-view/group-uploads-view.component.ts | 1 + .../tangy-forms-player/tangy-forms-player.component.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts index e74bace952..1ed0ea5f02 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts @@ -33,6 +33,7 @@ export class GroupUploadsViewComponent implements OnInit { } ] this.formPlayer.formResponseId = params.responseId + this.formPlayer.unlockFormResponses = true this.formPlayer.render() this.formPlayer.$submit.subscribe(async () => { this.router.navigate([`../`], { relativeTo: this.route }) diff --git a/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts index 492bee5636..95e6b0e321 100755 --- a/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts +++ b/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts @@ -25,7 +25,8 @@ export class TangyFormsPlayerComponent { // 2. Use this if you want to attach the form response yourself. @Input('response') response:TangyFormResponseModel // 3. Use this is you want a new form response. - @Input('formId') formId:string + @Input('formId') formId:string + @Input('unlockFormResponses') unlockFormResponses:boolean @Input('templateId') templateId:string @Input('location') location:any @@ -193,6 +194,7 @@ export class TangyFormsPlayerComponent { this.$afterResubmit.next(true) }) } + this.unlockFormResponses? this.formEl.unlock(): null this.$rendered.next(true) this.rendered = true } From 2cf8fdc5de66e761b5c6eb49e79f3541568467bb Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 24 Jan 2024 14:46:49 +0300 Subject: [PATCH 03/81] build(editor): Save changes when editing uploads Save getState Refs Tangerine-Community/Tangerine#3681 --- .../groups/group-uploads-view/group-uploads-view.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts index 1ed0ea5f02..430edb0f95 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts @@ -36,6 +36,7 @@ export class GroupUploadsViewComponent implements OnInit { this.formPlayer.unlockFormResponses = true this.formPlayer.render() this.formPlayer.$submit.subscribe(async () => { + this.formPlayer.saveResponse(this.formPlayer.formEl.store.getState()) this.router.navigate([`../`], { relativeTo: this.route }) }) }) From 0bb02074acdf57d082cab56863db7adf8a0574dd Mon Sep 17 00:00:00 2001 From: esurface Date: Wed, 14 Feb 2024 13:30:30 -0500 Subject: [PATCH 04/81] Update packages for the new tangy-prompt-box and tangy-radio-blocks --- CHANGELOG.md | 6 ++++++ client/package.json | 2 +- editor/package.json | 4 ++-- online-survey-app/package.json | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0456c4e0a3..854dda5188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # What's new +## v3.31.0 + +__New Features__ +- A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in [tangy-forms](https://github.com/Tangerine-Community/tangy-form/blob/master/CHANGELOG.md#v4430) + + ## v3.30.2 __New Features__ diff --git a/client/package.json b/client/package.json index f2093ac35f..0a03e64b85 100644 --- a/client/package.json +++ b/client/package.json @@ -75,7 +75,7 @@ "rxjs-compat": "^6.5.5", "sanitize-filename-ts": "^1.0.2", "spark-md5": "^3.0.2", - "tangy-form": "4.42.0", + "tangy-form": "^4.43.1-rc.1", "translation-web-component": "1.1.1", "tslib": "^1.10.0", "underscore": "^1.9.1", diff --git a/editor/package.json b/editor/package.json index 5437bf6419..920c3f8e52 100644 --- a/editor/package.json +++ b/editor/package.json @@ -49,8 +49,8 @@ "redux": "^4.0.5", "rxjs": "~6.5.5", "rxjs-compat": "^6.5.5", - "tangy-form": "4.42.0", - "tangy-form-editor": "7.17.6", + "tangy-form": "^4.43.1-rc.1", + "tangy-form-editor": "7.18.0-rc.0", "translation-web-component": "0.0.3", "tslib": "^1.11.2", "ua-parser-js": "^0.7.24", diff --git a/online-survey-app/package.json b/online-survey-app/package.json index e3146f09a8..c3a8509687 100644 --- a/online-survey-app/package.json +++ b/online-survey-app/package.json @@ -25,7 +25,7 @@ "material-design-icons-iconfont": "^6.1.0", "redux": "^4.0.5", "rxjs": "~6.6.0", - "tangy-form": "v4.42.0", + "tangy-form": "^4.43.1-rc.1", "translation-web-component": "^1.1.0", "tslib": "^2.0.0", "zone.js": "~0.10.2" From 047e3b46b75520a382b08b09b66bb39cf5ec7ad5 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 15 Feb 2024 15:18:47 -0500 Subject: [PATCH 05/81] Bump tangy-form to v4.31.1 and tangy-form-editor to v7.18.0 --- CHANGELOG.md | 2 ++ client/package.json | 2 +- editor/package.json | 4 ++-- online-survey-app/package.json | 2 +- tangerine-preview/package.json | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 854dda5188..1528ef83fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ __New Features__ - A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in [tangy-forms](https://github.com/Tangerine-Community/tangy-form/blob/master/CHANGELOG.md#v4430) +- Bump version of `tangy-form` to v4.31.1 and `tangy-form-editor` to v7.18.0 + ## v3.30.2 diff --git a/client/package.json b/client/package.json index 0a03e64b85..c3aee36169 100644 --- a/client/package.json +++ b/client/package.json @@ -75,7 +75,7 @@ "rxjs-compat": "^6.5.5", "sanitize-filename-ts": "^1.0.2", "spark-md5": "^3.0.2", - "tangy-form": "^4.43.1-rc.1", + "tangy-form": "^4.43.1", "translation-web-component": "1.1.1", "tslib": "^1.10.0", "underscore": "^1.9.1", diff --git a/editor/package.json b/editor/package.json index 920c3f8e52..27fcc05657 100644 --- a/editor/package.json +++ b/editor/package.json @@ -49,8 +49,8 @@ "redux": "^4.0.5", "rxjs": "~6.5.5", "rxjs-compat": "^6.5.5", - "tangy-form": "^4.43.1-rc.1", - "tangy-form-editor": "7.18.0-rc.0", + "tangy-form": "^4.43.1", + "tangy-form-editor": "7.18.0", "translation-web-component": "0.0.3", "tslib": "^1.11.2", "ua-parser-js": "^0.7.24", diff --git a/online-survey-app/package.json b/online-survey-app/package.json index c3a8509687..c36ddb8c9b 100644 --- a/online-survey-app/package.json +++ b/online-survey-app/package.json @@ -25,7 +25,7 @@ "material-design-icons-iconfont": "^6.1.0", "redux": "^4.0.5", "rxjs": "~6.6.0", - "tangy-form": "^4.43.1-rc.1", + "tangy-form": "^4.43.1", "translation-web-component": "^1.1.0", "tslib": "^2.0.0", "zone.js": "~0.10.2" diff --git a/tangerine-preview/package.json b/tangerine-preview/package.json index a5cd8783fc..d99cae3438 100644 --- a/tangerine-preview/package.json +++ b/tangerine-preview/package.json @@ -1,6 +1,6 @@ { "name": "tangerine-preview", - "version": "3.12.5", + "version": "3.31.0", "description": "", "main": "index.js", "bin": { From c5bb768215d0b676b127dc768dee757ee8ee47fb Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 15 Feb 2024 19:12:27 -0500 Subject: [PATCH 06/81] add tangy-prompt-box to polyfills --- client/src/polyfills.ts | 1 + online-survey-app/src/polyfills.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/client/src/polyfills.ts b/client/src/polyfills.ts index 6e8bf057b8..2e45b6d7b8 100644 --- a/client/src/polyfills.ts +++ b/client/src/polyfills.ts @@ -48,6 +48,7 @@ import 'tangy-form/input/tangy-signature.js'; import 'tangy-form/input/tangy-toggle.js'; import 'tangy-form/input/tangy-radio-blocks.js'; import 'tangy-form/input/tangy-video-capture.js'; +import 'tangy-form/input/tangy-prompt-box.js'; // import 'tangy-form/input/tangy-scan-image.js'; diff --git a/online-survey-app/src/polyfills.ts b/online-survey-app/src/polyfills.ts index 694f825381..9be4cf8b9b 100644 --- a/online-survey-app/src/polyfills.ts +++ b/online-survey-app/src/polyfills.ts @@ -89,6 +89,7 @@ import 'tangy-form/input/tangy-signature.js'; import 'tangy-form/input/tangy-toggle.js'; import 'tangy-form/input/tangy-radio-blocks.js'; import 'tangy-form/input/tangy-video-capture.js'; +import 'tangy-form/input/tangy-prompt-box.js'; import 'translation-web-component/t-select.js' From 026ec095bdcef2f88646fdfa18830fc3691a1074 Mon Sep 17 00:00:00 2001 From: esurface Date: Mon, 19 Feb 2024 10:56:20 -0500 Subject: [PATCH 07/81] Test tangy-form v4.43.2-rc.1 --- client/package.json | 2 +- editor/package.json | 2 +- online-survey-app/package.json | 2 +- tangerine-preview/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/package.json b/client/package.json index c3aee36169..13567a2320 100644 --- a/client/package.json +++ b/client/package.json @@ -75,7 +75,7 @@ "rxjs-compat": "^6.5.5", "sanitize-filename-ts": "^1.0.2", "spark-md5": "^3.0.2", - "tangy-form": "^4.43.1", + "tangy-form": "^4.43.2-rc.1", "translation-web-component": "1.1.1", "tslib": "^1.10.0", "underscore": "^1.9.1", diff --git a/editor/package.json b/editor/package.json index 27fcc05657..32f4a26d30 100644 --- a/editor/package.json +++ b/editor/package.json @@ -49,7 +49,7 @@ "redux": "^4.0.5", "rxjs": "~6.5.5", "rxjs-compat": "^6.5.5", - "tangy-form": "^4.43.1", + "tangy-form": "^4.43.2-rc.1", "tangy-form-editor": "7.18.0", "translation-web-component": "0.0.3", "tslib": "^1.11.2", diff --git a/online-survey-app/package.json b/online-survey-app/package.json index c36ddb8c9b..1c6493443c 100644 --- a/online-survey-app/package.json +++ b/online-survey-app/package.json @@ -25,7 +25,7 @@ "material-design-icons-iconfont": "^6.1.0", "redux": "^4.0.5", "rxjs": "~6.6.0", - "tangy-form": "^4.43.1", + "tangy-form": "^4.43.2-rc.1", "translation-web-component": "^1.1.0", "tslib": "^2.0.0", "zone.js": "~0.10.2" diff --git a/tangerine-preview/package.json b/tangerine-preview/package.json index d99cae3438..96b452cfc6 100644 --- a/tangerine-preview/package.json +++ b/tangerine-preview/package.json @@ -1,6 +1,6 @@ { "name": "tangerine-preview", - "version": "3.31.0", + "version": "3.31.0-rc-6", "description": "", "main": "index.js", "bin": { From 6d141cea03579230af0ff9822af93f08122ade8e Mon Sep 17 00:00:00 2001 From: esurface Date: Tue, 20 Feb 2024 12:07:19 -0500 Subject: [PATCH 08/81] Bump tangy-form to v4.43.2-rc.3 --- client/package.json | 2 +- editor/package.json | 2 +- online-survey-app/package.json | 2 +- tangerine-preview/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/package.json b/client/package.json index 13567a2320..a8d85a4531 100644 --- a/client/package.json +++ b/client/package.json @@ -75,7 +75,7 @@ "rxjs-compat": "^6.5.5", "sanitize-filename-ts": "^1.0.2", "spark-md5": "^3.0.2", - "tangy-form": "^4.43.2-rc.1", + "tangy-form": "^4.43.2-rc.3", "translation-web-component": "1.1.1", "tslib": "^1.10.0", "underscore": "^1.9.1", diff --git a/editor/package.json b/editor/package.json index 32f4a26d30..cc58642305 100644 --- a/editor/package.json +++ b/editor/package.json @@ -49,7 +49,7 @@ "redux": "^4.0.5", "rxjs": "~6.5.5", "rxjs-compat": "^6.5.5", - "tangy-form": "^4.43.2-rc.1", + "tangy-form": "^4.43.2-rc.3", "tangy-form-editor": "7.18.0", "translation-web-component": "0.0.3", "tslib": "^1.11.2", diff --git a/online-survey-app/package.json b/online-survey-app/package.json index 1c6493443c..3792ce7c22 100644 --- a/online-survey-app/package.json +++ b/online-survey-app/package.json @@ -25,7 +25,7 @@ "material-design-icons-iconfont": "^6.1.0", "redux": "^4.0.5", "rxjs": "~6.6.0", - "tangy-form": "^4.43.2-rc.1", + "tangy-form": "^4.43.2-rc.3", "translation-web-component": "^1.1.0", "tslib": "^2.0.0", "zone.js": "~0.10.2" diff --git a/tangerine-preview/package.json b/tangerine-preview/package.json index 96b452cfc6..f00c17c64f 100644 --- a/tangerine-preview/package.json +++ b/tangerine-preview/package.json @@ -1,6 +1,6 @@ { "name": "tangerine-preview", - "version": "3.31.0-rc-6", + "version": "3.31.0-rc-8", "description": "", "main": "index.js", "bin": { From 82c54936ed1e4712b748eced788374e1fa0a2f5d Mon Sep 17 00:00:00 2001 From: esurface Date: Fri, 1 Mar 2024 12:59:36 -0500 Subject: [PATCH 09/81] Add env bash to reporting-worker-unpause --- server/src/scripts/reporting-worker-unpause.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/scripts/reporting-worker-unpause.sh b/server/src/scripts/reporting-worker-unpause.sh index bcda35be9b..203fcb136c 100755 --- a/server/src/scripts/reporting-worker-unpause.sh +++ b/server/src/scripts/reporting-worker-unpause.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + if [ "$2" = "--help" ]; then echo "Usage:" echo " reporting-worker-pause" From 09fc5ba40af05a77d0761be9dcea86571262c37d Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 14 Mar 2024 15:16:25 -0400 Subject: [PATCH 10/81] Add 'late' status to attendence check table --- .../src/app/class/_services/dashboard.service.ts | 1 + .../attendance-check.component.html | 14 +++++++++++--- .../attendance-check/attendance-check.component.ts | 14 ++++---------- .../attendance-dashboard.component.css | 3 +++ .../app/class/dashboard/dashboard.component.css | 4 ++++ .../reports/attendance/attendance.component.css | 9 +++++++++ 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/client/src/app/class/_services/dashboard.service.ts b/client/src/app/class/_services/dashboard.service.ts index 98203bc29a..9f0053b031 100644 --- a/client/src/app/class/_services/dashboard.service.ts +++ b/client/src/app/class/_services/dashboard.service.ts @@ -1087,6 +1087,7 @@ export class DashboardService { // studentResult['phone'] = phone // studentResult['classId'] = classId studentResult['absent'] = null + studentResult['late'] = null list.push(studentResult) } diff --git a/client/src/app/class/attendance/attendance-check/attendance-check.component.html b/client/src/app/class/attendance/attendance-check/attendance-check.component.html index 9177b2ae37..aafddd42cf 100644 --- a/client/src/app/class/attendance/attendance-check/attendance-check.component.html +++ b/client/src/app/class/attendance/attendance-check/attendance-check.component.html @@ -13,14 +13,22 @@ {{element["student_name"]}} {{element["student_surname"]}} - + + + + - + + close + + + + remove - + check diff --git a/client/src/app/class/attendance/attendance-check/attendance-check.component.ts b/client/src/app/class/attendance/attendance-check/attendance-check.component.ts index 90c82f7395..9c82dd1abc 100644 --- a/client/src/app/class/attendance/attendance-check/attendance-check.component.ts +++ b/client/src/app/class/attendance/attendance-check/attendance-check.component.ts @@ -133,19 +133,13 @@ export class AttendanceCheckComponent implements OnInit { } - async toggleAttendance(student) { - student.absent = !student.absent - await this.saveStudentAttendance() - } + async toggleAttendance(status, student) { + student.absent = status == 'absent' + student.late = status == 'late' - async toggleMood(mood, student) { - student.mood = mood - if (!student.absent) { - await this.saveStudentAttendance() - } + await this.saveStudentAttendance() } - private async saveStudentAttendance() { // save allStudentResults this.attendanceRegister.attendanceList = this.attendanceList diff --git a/client/src/app/class/attendance/attendance-dashboard/attendance-dashboard.component.css b/client/src/app/class/attendance/attendance-dashboard/attendance-dashboard.component.css index f328b8da65..20eeac4954 100644 --- a/client/src/app/class/attendance/attendance-dashboard/attendance-dashboard.component.css +++ b/client/src/app/class/attendance/attendance-dashboard/attendance-dashboard.component.css @@ -199,6 +199,9 @@ mat-card-title { .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary.red { background-color: red; } +.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary.orange { + background-color: orange; +} .gray { background-color: gray; diff --git a/client/src/app/class/dashboard/dashboard.component.css b/client/src/app/class/dashboard/dashboard.component.css index 1e742f709b..afa6fc323b 100644 --- a/client/src/app/class/dashboard/dashboard.component.css +++ b/client/src/app/class/dashboard/dashboard.component.css @@ -194,6 +194,10 @@ mat-card-title { background-color: red; } +.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary.orange { + background-color: orange; +} + .gray { background-color: gray; } diff --git a/client/src/app/class/reports/attendance/attendance.component.css b/client/src/app/class/reports/attendance/attendance.component.css index fe7f437068..dbb152ecc5 100644 --- a/client/src/app/class/reports/attendance/attendance.component.css +++ b/client/src/app/class/reports/attendance/attendance.component.css @@ -60,6 +60,11 @@ .green { background-color: green; } + +.orange { + background-color: orange; +} + .mat-chip.mat-standard-chip.mat-primary { background-color: green; } @@ -76,6 +81,10 @@ background-color: red; } +.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary.orange { + background-color: orange; +} + .tangy-class-card-content-container { margin-left: 3%; /*width: 90%;*/ From e1727a0311fd9a5d38f603c7ba926ba4cda12aaa Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 14 Mar 2024 16:23:00 -0400 Subject: [PATCH 11/81] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1528ef83fe..331498514e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ __New Features__ - Bump version of `tangy-form` to v4.31.1 and `tangy-form-editor` to v7.18.0 +__Tangerine Teach__ + +- Add toggle in Attendence Check for 'late'. A teacher can click through the status of 'present', 'late', or 'absent' for each student. + ## v3.30.2 From b3ddb572f6dbf24551406fed4ba827086645aba8 Mon Sep 17 00:00:00 2001 From: esurface Date: Mon, 18 Mar 2024 13:07:11 -0400 Subject: [PATCH 12/81] Use studentRegistrationFields to show name and surname of student in the student dashboard --- .../app/class/_services/dashboard.service.ts | 51 ++++++++++++------- .../class/dashboard/dashboard.component.html | 2 +- .../class/dashboard/dashboard.component.ts | 29 +---------- 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/client/src/app/class/_services/dashboard.service.ts b/client/src/app/class/_services/dashboard.service.ts index 9f0053b031..c026c29a53 100644 --- a/client/src/app/class/_services/dashboard.service.ts +++ b/client/src/app/class/_services/dashboard.service.ts @@ -1043,6 +1043,37 @@ export class DashboardService { return gradeInput?.value } + async getAllStudentResults(students, studentsResponses, curriculumFormsList, curriculum) { + const allStudentResults = []; + + const appConfig = await this.appConfigService.getAppConfig(); + const studentRegistrationFields = appConfig.teachProperties?.studentRegistrationFields || [] + + students.forEach((student) => { + const studentResult = {}; + + studentResult['id'] = student.id; + studentRegistrationFields.forEach((field) => { + studentResult[field] = this.getValue(field, student.doc) + }) + studentResult['forms'] = {}; + curriculumFormsList.forEach((form) => { + const formResult = {}; + formResult['formId'] = form.id; + formResult['curriculum'] = curriculum.name; + formResult['title'] = form.title; + formResult['src'] = form.src; + if (studentsResponses[student.id]) { + formResult['response'] = studentsResponses[student.id][form.id]; + } + studentResult['forms'][form.id] = formResult; + }); + allStudentResults.push(studentResult); + }); + + return allStudentResults; + } + /** * Get the attendance list for the class, including any students who have not yet had attendance checked. If the savedAttendanceList is passed in, then * populate the student from that doc by matching student.id. @@ -1050,10 +1081,9 @@ export class DashboardService { * @param savedList */ async getAttendanceList(students, savedList, curriculum) { - // const curriculumFormHtml = await this.getCurriculaForms(curriculum.name); - // const curriculumFormsList = await this.classUtils.createCurriculumFormsList(curriculumFormHtml); const appConfig = await this.appConfigService.getAppConfig(); const studentRegistrationFields = appConfig.teachProperties?.studentRegistrationFields || [] + const list = [] for (const student of students) { let studentResult @@ -1062,13 +1092,6 @@ export class DashboardService { studentResult = savedList.find(studentDoc => studentDoc.id === studentId) } if (studentResult) { - // migration. - if (!studentResult.student_surname) { - studentResult.student_surname = studentResult.surname - } - if (!studentResult.student_name) { - studentResult.student_name = studentResult.name - } list.push(studentResult) } else { studentResult = {} @@ -1077,15 +1100,6 @@ export class DashboardService { studentRegistrationFields.forEach((field) => { studentResult[field] = this.getValue(field, student.doc) }) - // const student_name = this.getValue('student_name', student.doc) - // const student_surname = this.getValue('student_surname', student.doc) - // const phone = this.getValue('phone', student.doc); - // const classId = this.getValue('classId', student.doc) - - // studentResult['name'] = student_name - // studentResult['surname'] = student_surname - // studentResult['phone'] = phone - // studentResult['classId'] = classId studentResult['absent'] = null studentResult['late'] = null @@ -1093,7 +1107,6 @@ export class DashboardService { } } return list - // await this.populateFeedback(curriculumId); } /** diff --git a/client/src/app/class/dashboard/dashboard.component.html b/client/src/app/class/dashboard/dashboard.component.html index 5053018c81..481be252f3 100644 --- a/client/src/app/class/dashboard/dashboard.component.html +++ b/client/src/app/class/dashboard/dashboard.component.html @@ -58,7 +58,7 @@ {{'Completed?'|translate}} - {{element["name"]}} + {{element["student_name"]}} {{element["student_surname"]}} { - const studentResults = {}; - const student_name = this.getValue('student_name', student.doc) - const classId = this.getValue('classId', student.doc) - studentResults['id'] = student.id; - studentResults['name'] = student_name - studentResults['classId'] = classId - // studentResults["forms"] = []; - studentResults['forms'] = {}; - // for (const form of this.curriculumForms) { - this.curriculumFormsList.forEach((form) => { - const formResult = {}; - formResult['formId'] = form.id; - formResult['curriculum'] = this.curriculum.name; - formResult['title'] = form.title; - formResult['src'] = form.src; - if (this.studentsResponses[student.id]) { - formResult['response'] = this.studentsResponses[student.id][form.id]; - } - // studentResults["forms"].push(formResult) - studentResults['forms'][form.id] = formResult; - }); - allStudentResults.push(studentResults); - }); - this.allStudentResults = allStudentResults; - // await this.populateFeedback(curriculumId); + this.allStudentResults = await this.dashboardService.getAllStudentResults(this.students, this.studentsResponses, this.curriculumFormsList, this.curriculum); } // Triggered by dropdown selection in UI. From 96e5e23ccdc00cdab65f4942eb2ae1cdf5d4629e Mon Sep 17 00:00:00 2001 From: esurface Date: Mon, 18 Mar 2024 13:08:30 -0400 Subject: [PATCH 13/81] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 331498514e..a05ebb13b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ __New Features__ __Tangerine Teach__ - Add toggle in Attendence Check for 'late'. A teacher can click through the status of 'present', 'late', or 'absent' for each student. +- Use `studentRegistrationFields` to control showing name and surname of student in the student dashboard ## v3.30.2 From 99b56ae6d3a46eb0811dd28b2fec6d187cac8c57 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 21 Mar 2024 15:31:29 +0300 Subject: [PATCH 14/81] feat(search-uploads): Search uploads by ID Refs Tangerine-Community/Tangerine#3681 --- .../groups/responses/responses.component.html | 5 ++ .../groups/responses/responses.component.ts | 65 +++++++++++++++++-- server/src/routes/group-responses.js | 12 +++- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/editor/src/app/groups/responses/responses.component.html b/editor/src/app/groups/responses/responses.component.html index 55ba745427..8074bd2c93 100644 --- a/editor/src/app/groups/responses/responses.component.html +++ b/editor/src/app/groups/responses/responses.component.html @@ -4,6 +4,11 @@ +
+ +
+
+
diff --git a/editor/src/app/groups/responses/responses.component.ts b/editor/src/app/groups/responses/responses.component.ts index eb03a49f82..d262642123 100644 --- a/editor/src/app/groups/responses/responses.component.ts +++ b/editor/src/app/groups/responses/responses.component.ts @@ -2,10 +2,13 @@ import { Router, ActivatedRoute } from '@angular/router'; import { AppConfigService } from 'src/app/shared/_services/app-config.service'; import { generateFlatResponse } from './tangy-form-response-flatten'; import { TangerineFormsService } from './../services/tangerine-forms.service'; -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit, Input,ViewChild, ElementRef } from '@angular/core'; import { GroupsService } from '../services/groups.service'; import { HttpClient } from '@angular/common/http'; import * as moment from 'moment' +import { t } from 'tangy-form/util/t.js' +import { Subject } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; @Component({ selector: 'app-responses', @@ -19,6 +22,9 @@ export class ResponsesComponent implements OnInit { @Input() excludeForms:Array = [] @Input() excludeColumns:Array = [] @Input() hideFilterBy = false + @ViewChild('searchBar', {static: true}) searchBar: ElementRef + @ViewChild('searchResults', {static: true}) searchResults: ElementRef + onSearch$ = new Subject() ready = false @@ -27,6 +33,8 @@ export class ResponsesComponent implements OnInit { skip = 0; limit = 30; forms = []; + locationLists + searchString constructor( private groupsService: GroupsService, @@ -43,12 +51,32 @@ export class ResponsesComponent implements OnInit { this.forms = (await this.tangerineFormsService.getFormsInfo(this.groupId)) .filter(formInfo => !this.excludeForms.includes(formInfo.id) ) await this.getResponses() - this.ready = true + this.ready = true; + this.onSearch$ + .pipe(debounceTime(300)) + .subscribe((searchString:string) => { + this.responses.length <= 0 ? this.searchResults.nativeElement.innerHTML = 'Searching...' : null + this.onSearch(searchString) + }); + this + .searchBar + .nativeElement + .addEventListener('keyup', event => { + const searchString = event.target.value + if (searchString.length > 2 || searchString.length === 0) { + this.onSearch$.next(event.target.value) + } else { + this.searchResults.nativeElement.innerHTML = ` + + ${t('Enter more than two characters...')} + + ` + } + }) } async getResponses() { - const data: any = await this.groupsService.getLocationLists(this.groupId); - const locationLists = data; + this.locationLists = await this.groupsService.getLocationLists(this.groupId); let responses = [] if (this.filterBy === '*') { responses = >await this.http.get(`/api/${this.groupId}/responses/${this.limit}/${this.skip}`).toPromise() @@ -57,13 +85,30 @@ export class ResponsesComponent implements OnInit { } const flatResponses = [] for (let response of responses) { - const flatResponse = await generateFlatResponse(response, locationLists, false) + const flatResponse = await generateFlatResponse(response, this.locationLists, false) this.excludeColumns.forEach(column => delete flatResponse[column]) flatResponses.push(flatResponse) } this.responses = flatResponses } + async onSearch(searchString) { + this.searchString = searchString + this.responses = [] + if (searchString === '') { + this.searchResults.nativeElement.innerHTML = '' + return + } + const responses = >await this.http.get(`/api/${this.groupId}/responses/${this.limit}/${this.skip}/?id=${searchString}`).toPromise() + const flatResponses = [] + for (let response of responses) { + const flatResponse = await generateFlatResponse(response, this.locationLists, false) + this.excludeColumns.forEach(column => delete flatResponse[column]) + flatResponses.push(flatResponse) + } + this.responses = flatResponses + } + async filterByForm(event) { this.filterBy = event.target['value']; this.skip = 0; @@ -79,12 +124,20 @@ export class ResponsesComponent implements OnInit { nextPage() { this.skip = this.skip + this.limit - this.getResponses(); + if(this.searchString){ + this.onSearch(this.searchString) + } else{ + this.getResponses(); + } } previousPage() { this.skip = this.skip - this.limit + if(this.searchString){ + this.onSearch(this.searchString) + } else{ this.getResponses(); + } } onRowEdit(row) { diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index e8b00a810a..568ad08984 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -12,9 +12,15 @@ module.exports = async (req, res) => { if (req.params.skip) { options.skip = req.params.skip } - const results = await groupDb.query('responsesByStartUnixTime', options); - const docs = results.rows.map(row => row.doc) - res.send(docs) + if (Object.keys(req.query).length < 1 ){ + const results = await groupDb.query('responsesByStartUnixTime', options); + const docs = results.rows.map(row => row.doc) + res.send(docs) + } else{ + const results = await groupDb.query('responsesByStartUnixTime', {...options, startkey:req.query.id}); + const docs = results.rows.filter(row => row.id.startsWith(req.query.id)).map(row=>row.doc) + res.send(docs) + } } catch (error) { log.error(error); res.status(500).send(error); From cbf4071ff300027e9edfcae13f82b4d68bb03768 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 21 Mar 2024 12:37:15 -0400 Subject: [PATCH 15/81] Add teacher property to control the appearance of the late option in attendance checks --- .../app/class/_services/dashboard.service.ts | 4 ++- .../attendance-check.component.html | 18 ++++++---- .../attendance-check.component.ts | 36 ++++++++++++++++--- .../app/shared/_classes/app-config.class.ts | 3 +- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/client/src/app/class/_services/dashboard.service.ts b/client/src/app/class/_services/dashboard.service.ts index c026c29a53..87995e4386 100644 --- a/client/src/app/class/_services/dashboard.service.ts +++ b/client/src/app/class/_services/dashboard.service.ts @@ -1101,7 +1101,9 @@ export class DashboardService { studentResult[field] = this.getValue(field, student.doc) }) studentResult['absent'] = null - studentResult['late'] = null + if (appConfig.teachProperties?.showLateAttendanceOption) { + studentResult['late'] = null + } list.push(studentResult) } diff --git a/client/src/app/class/attendance/attendance-check/attendance-check.component.html b/client/src/app/class/attendance/attendance-check/attendance-check.component.html index aafddd42cf..dd6ca0d95f 100644 --- a/client/src/app/class/attendance/attendance-check/attendance-check.component.html +++ b/client/src/app/class/attendance/attendance-check/attendance-check.component.html @@ -13,22 +13,28 @@ {{element["student_name"]}} {{element["student_surname"]}} - - - + + + + + + + + + - + close - + remove - + check diff --git a/client/src/app/class/attendance/attendance-check/attendance-check.component.ts b/client/src/app/class/attendance/attendance-check/attendance-check.component.ts index 9c82dd1abc..83b0f0d04d 100644 --- a/client/src/app/class/attendance/attendance-check/attendance-check.component.ts +++ b/client/src/app/class/attendance/attendance-check/attendance-check.component.ts @@ -9,6 +9,7 @@ import {UserService} from "../../../shared/_services/user.service"; import {FormMetadata} from "../../form-metadata"; import {ClassFormService} from "../../_services/class-form.service"; import { TangySnackbarService } from 'src/app/shared/_services/tangy-snackbar.service'; +import { AppConfigService } from 'src/app/shared/_services/app-config.service'; @Component({ selector: 'app-attendance-check', @@ -40,16 +41,22 @@ export class AttendanceCheckComponent implements OnInit { curriculum:any ignoreCurriculumsForTracking: boolean = false reportLocaltime: string; + showLateAttendanceOption: boolean = false; constructor( private dashboardService: DashboardService, private variableService : VariableService, private router: Router, private classFormService: ClassFormService, - private tangySnackbarService: TangySnackbarService + private tangySnackbarService: TangySnackbarService, + private appConfigService: AppConfigService ) { } async ngOnInit(): Promise { + + const appConfig = await this.appConfigService.getAppConfig() + this.showLateAttendanceOption = appConfig.teachProperties.showLateAttendanceOption || this.showLateAttendanceOption + let classIndex await this.classFormService.initialize(); this.getValue = this.dashboardService.getValue @@ -133,9 +140,30 @@ export class AttendanceCheckComponent implements OnInit { } - async toggleAttendance(status, student) { - student.absent = status == 'absent' - student.late = status == 'late' + async toggleAttendance(currentStatus, student) { + if (this.showLateAttendanceOption) { + if (currentStatus == 'present') { + // moving from present status to late status, then they are not absent + student.absent = false + student.late = true + } else if (currentStatus == 'late') { + // moving from late status to absent status, then they are absent + student.absent = true + student.late = false + } else { + // moving from absent status to present status, then they are not absent + student.absent = false + student.late = false + } + } else { + if (currentStatus == 'present') { + // moving from present status to absent status, then they are absent + student.absent = true + } else { + // moving from absent status to present status, then they are not absent + student.absent = false + } + } await this.saveStudentAttendance() } diff --git a/client/src/app/shared/_classes/app-config.class.ts b/client/src/app/shared/_classes/app-config.class.ts index 9694b2a680..0a9bb36d22 100644 --- a/client/src/app/shared/_classes/app-config.class.ts +++ b/client/src/app/shared/_classes/app-config.class.ts @@ -150,7 +150,8 @@ export class AppConfig { behaviorSecondaryThreshold: 80, useAttendanceFeature: false, showAttendanceCalendar: false, - studentRegistrationFields:[] + studentRegistrationFields:[], + showLateAttendanceOption: false } // From a6c5c0d1ca0c97cffe9bb9e699c665f82e7a84f8 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 21 Mar 2024 12:39:37 -0400 Subject: [PATCH 16/81] Simplify comments --- .../attendance-check/attendance-check.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/app/class/attendance/attendance-check/attendance-check.component.ts b/client/src/app/class/attendance/attendance-check/attendance-check.component.ts index 83b0f0d04d..16d0f25b72 100644 --- a/client/src/app/class/attendance/attendance-check/attendance-check.component.ts +++ b/client/src/app/class/attendance/attendance-check/attendance-check.component.ts @@ -143,24 +143,24 @@ export class AttendanceCheckComponent implements OnInit { async toggleAttendance(currentStatus, student) { if (this.showLateAttendanceOption) { if (currentStatus == 'present') { - // moving from present status to late status, then they are not absent + // moving from present status to late status student.absent = false student.late = true } else if (currentStatus == 'late') { - // moving from late status to absent status, then they are absent + // moving from late status to absent status student.absent = true student.late = false } else { - // moving from absent status to present status, then they are not absent + // moving from absent status to present status student.absent = false student.late = false } } else { if (currentStatus == 'present') { - // moving from present status to absent status, then they are absent + // moving from present status to absent status student.absent = true } else { - // moving from absent status to present status, then they are not absent + // moving from absent status to present status student.absent = false } } From 769c6caa88d8faa470fe63f6959af063c39cc33a Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Mon, 25 Mar 2024 14:51:54 +0300 Subject: [PATCH 17/81] feat(client): Add custom markup on Login page * Allow users to include custom markup to be rendered in the login page * Add property `customLoginMarkup` to `app.config.json` that accepts valid `HTML` markup that will be rendered in the login page after the `Login` and `Reset Password` buttons. * If `customLoginMarkup` is not defined in the `app.config.json`, customLoginMarkup defaults to an empty string thus nothing is rendered Refs Tangerine-Community/Tangerine# --- client/src/app/core/auth/login/login.component.html | 1 + client/src/app/core/auth/login/login.component.ts | 4 +++- client/src/app/shared/_classes/app-config.class.ts | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/app/core/auth/login/login.component.html b/client/src/app/core/auth/login/login.component.html index 900ed25139..21084a85e2 100755 --- a/client/src/app/core/auth/login/login.component.html +++ b/client/src/app/core/auth/login/login.component.html @@ -82,3 +82,4 @@ + \ No newline at end of file diff --git a/client/src/app/core/auth/login/login.component.ts b/client/src/app/core/auth/login/login.component.ts index b7b8341a54..fa0ff24068 100755 --- a/client/src/app/core/auth/login/login.component.ts +++ b/client/src/app/core/auth/login/login.component.ts @@ -5,7 +5,7 @@ import { DeviceService } from './../../../device/services/device.service'; import {from as observableFrom, Observable } from 'rxjs'; -import { Component, OnInit } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AppConfigService } from '../../../shared/_services/app-config.service'; @@ -33,6 +33,7 @@ export class LoginComponent implements OnInit { hidePassword = true passwordPolicy: string passwordRecipe: string + @ViewChild('customLoginMarkup', {static: true}) customLoginMarkup: ElementRef; constructor( private route: ActivatedRoute, private router: Router, @@ -59,6 +60,7 @@ export class LoginComponent implements OnInit { if (this.userService.isLoggedIn()) { this.router.navigate([this.returnUrl]); } + this.customLoginMarkup.nativeElement.innerHTML = appConfig.customLoginMarkup || ''; } diff --git a/client/src/app/shared/_classes/app-config.class.ts b/client/src/app/shared/_classes/app-config.class.ts index 0a9bb36d22..2479a5589b 100644 --- a/client/src/app/shared/_classes/app-config.class.ts +++ b/client/src/app/shared/_classes/app-config.class.ts @@ -10,7 +10,8 @@ export class AppConfig { // Tangerine Case Management: 'case-home' // Tangerine Teach: 'dashboard' homeUrl = "case-management" - + // customLoginMarkup Custom Markup to include on the login page + customLoginMarkup: string // // i18n configuration. // From 700c8c6f8fdc235692a7b1321e8ce2f588ab9ce7 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Tue, 2 Apr 2024 10:34:00 +0300 Subject: [PATCH 18/81] feat(editor): Search By id Search without pagination and fix search strings Refs Tangerine-Community/Tangerine#3685 --- .../app/groups/responses/responses.component.html | 2 +- .../src/app/groups/responses/responses.component.ts | 12 +++++++++--- server/src/routes/group-responses.js | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/editor/src/app/groups/responses/responses.component.html b/editor/src/app/groups/responses/responses.component.html index 8074bd2c93..ffa3f422ce 100644 --- a/editor/src/app/groups/responses/responses.component.html +++ b/editor/src/app/groups/responses/responses.component.html @@ -13,4 +13,4 @@ < back -next > \ No newline at end of file +next > diff --git a/editor/src/app/groups/responses/responses.component.ts b/editor/src/app/groups/responses/responses.component.ts index d262642123..ac4c574d24 100644 --- a/editor/src/app/groups/responses/responses.component.ts +++ b/editor/src/app/groups/responses/responses.component.ts @@ -35,6 +35,7 @@ export class ResponsesComponent implements OnInit { forms = []; locationLists searchString + initialResults = [] constructor( private groupsService: GroupsService, @@ -62,10 +63,12 @@ export class ResponsesComponent implements OnInit { .searchBar .nativeElement .addEventListener('keyup', event => { - const searchString = event.target.value - if (searchString.length > 2 || searchString.length === 0) { + const searchString = event.target.value.trim() + if (searchString.length > 2) { + if (this.searchString === searchString) return; + this.searchResults.nativeElement.innerHTML = 'Searching...' this.onSearch$.next(event.target.value) - } else { + } if(searchString.length <= 2 && searchString.length !==0 ) { this.searchResults.nativeElement.innerHTML = ` ${t('Enter more than two characters...')} @@ -90,6 +93,7 @@ export class ResponsesComponent implements OnInit { flatResponses.push(flatResponse) } this.responses = flatResponses + this.initialResults = flatResponses } async onSearch(searchString) { @@ -97,6 +101,7 @@ export class ResponsesComponent implements OnInit { this.responses = [] if (searchString === '') { this.searchResults.nativeElement.innerHTML = '' + this.responses = this.initialResults return } const responses = >await this.http.get(`/api/${this.groupId}/responses/${this.limit}/${this.skip}/?id=${searchString}`).toPromise() @@ -107,6 +112,7 @@ export class ResponsesComponent implements OnInit { flatResponses.push(flatResponse) } this.responses = flatResponses + this.responses.length > 0 ? this.searchResults.nativeElement.innerHTML = '': this.searchResults.nativeElement.innerHTML = 'No results matching the criteria.' } async filterByForm(event) { diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index 568ad08984..997774edbd 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -17,8 +17,8 @@ module.exports = async (req, res) => { const docs = results.rows.map(row => row.doc) res.send(docs) } else{ - const results = await groupDb.query('responsesByStartUnixTime', {...options, startkey:req.query.id}); - const docs = results.rows.filter(row => row.id.startsWith(req.query.id)).map(row=>row.doc) + const results = await groupDb.query('responsesByStartUnixTime', {include_docs: true, descending: true, startkey:req.query.id}); + const docs = results.rows.filter(row => row.id.startsWith(req.query.id)).map(row => row.doc) res.send(docs) } } catch (error) { From 31360eebbb937c1201e96c37117d741d2adceb2f Mon Sep 17 00:00:00 2001 From: esurface Date: Tue, 2 Apr 2024 10:22:47 -0400 Subject: [PATCH 19/81] Update CHANGELOG and separate out upgrade instructions --- CHANGELOG.md | 14 ++++- .../upgrade-instructions.md | 63 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 docs/system-administrator/upgrade-instructions.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a05ebb13b4..80157d59f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,25 @@ __New Features__ - A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in [tangy-forms](https://github.com/Tangerine-Community/tangy-form/blob/master/CHANGELOG.md#v4430) -- Bump version of `tangy-form` to v4.31.1 and `tangy-form-editor` to v7.18.0 +- Client Login Screen Custom HTML: A new app-config.json setting, `customLoginMarkup`, allows for custom HTML to be added to the login screen. This feature is useful for adding custom branding or additional information to the login screen. As an example: +```json +"customLoginMarkup": "
logo
" +``` __Tangerine Teach__ - Add toggle in Attendence Check for 'late'. A teacher can click through the status of 'present', 'late', or 'absent' for each student. - Use `studentRegistrationFields` to control showing name and surname of student in the student dashboard +__Libs and Dependencies__ +- Bump version of `tangy-form` to v4.31.1 and `tangy-form-editor` to v7.18.0 for the new Prompt Box widget + +__Server upgrade instructions__ + +See the [Server Upgrade Insturctions](https://docs.tangerinecentral.org/system-administrator/upgrade-instructions). + +*Special Instructions for this release:* NONE + ## v3.30.2 diff --git a/docs/system-administrator/upgrade-instructions.md b/docs/system-administrator/upgrade-instructions.md new file mode 100644 index 0000000000..b0838f13b9 --- /dev/null +++ b/docs/system-administrator/upgrade-instructions.md @@ -0,0 +1,63 @@ +## Server upgrade instructions + +Reminder: Consider using the [Tangerine Upgrade Checklist](https://docs.tangerinecentral.org/system-administrator/upgrade-checklist.html) for making sure you test the upgrade safely. + +__Preparation__ + +Tangerine v3 images are relatively large, around 12GB. The server should have at least 20GB of free space plus the size of the data folder. Check the disk space before upgrading the the new version using the following steps: + +```bash +cd tangerine +# Check the size of the data folder. +du -sh data +# Check disk for free space. +df -h +``` + +If there is **less than** 20 GB plus the size of the data folder, create more space before proceeding. Good candidates to remove are: older versions of the Tangerine image and data backups. +```bash +# List all docker images. +docker image ls +# Remove the image of the version that is not being used. +docker rmi tangerine/tangerine: +# List all data backups. +ls -l data-backup-* +# Remove the data backups that are old and unneeded. +rm -rf ../data-backup- +``` + +__Upgrade__ + +After ensuring there is enough disk space, follow the steps below to upgrade the server. + +1. Backup the data folder +```bash +# Create a backup of the data folder. +cp -r data ../data-backup-$(date "+%F-%T") +``` + +2. Confirm there is no active synching from client devices + +Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. + +```bash +docker logs --since=60m tangerine +``` + +3. Install the new version of Tangerine +```bash +# Fetch the updates. +git fetch origin +# Checkout a new branch with the new version tag. +git checkout -b +# Run the start script with the new version. +./start.sh +``` + +__Clean Up__ + +After the upgrade, remove the previous version of the Tangerine image to free up disk space. + +```bash +docker rmi tangerine/tangerine: +``` \ No newline at end of file From 0b20392d656273a4cccc176f3b02a61a7c769148 Mon Sep 17 00:00:00 2001 From: esurface Date: Tue, 2 Apr 2024 10:25:58 -0400 Subject: [PATCH 20/81] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80157d59f5..bee29a0263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ __New Features__ "customLoginMarkup": "
logo
" ``` +- Add ID Search to Data > Import: A new feature in the Data > Import screen allows users to search for a specific ID in the imported data. This feature is useful for finding specific records synced to the server when reviewing or editing completed form responses. + __Tangerine Teach__ - Add toggle in Attendence Check for 'late'. A teacher can click through the status of 'present', 'late', or 'absent' for each student. From c7b031937868749a3a8cb5c4b4c5922a4b87d3cd Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Tue, 9 Apr 2024 11:49:25 +0300 Subject: [PATCH 21/81] fix(editor): Add index to docs before search Ensure docs are indexed before running search query Refs Tangerine-Community/Tangerine#3691 --- .../groups/responses/responses.component.ts | 6 +++- server/src/routes/group-responses.js | 32 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/editor/src/app/groups/responses/responses.component.ts b/editor/src/app/groups/responses/responses.component.ts index ac4c574d24..55d1883a6b 100644 --- a/editor/src/app/groups/responses/responses.component.ts +++ b/editor/src/app/groups/responses/responses.component.ts @@ -62,7 +62,7 @@ export class ResponsesComponent implements OnInit { this .searchBar .nativeElement - .addEventListener('keyup', event => { + .addEventListener('keyup', async event => { const searchString = event.target.value.trim() if (searchString.length > 2) { if (this.searchString === searchString) return; @@ -74,6 +74,10 @@ export class ResponsesComponent implements OnInit { ${t('Enter more than two characters...')}
` + } if(searchString.length===0){ + this.searchResults.nativeElement.innerHTML = '' + this.searchString = '' + await this.getResponses() } }) } diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index 997774edbd..f5ea1a72e4 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -17,12 +17,38 @@ module.exports = async (req, res) => { const docs = results.rows.map(row => row.doc) res.send(docs) } else{ - const results = await groupDb.query('responsesByStartUnixTime', {include_docs: true, descending: true, startkey:req.query.id}); - const docs = results.rows.filter(row => row.id.startsWith(req.query.id)).map(row => row.doc) + try { + await groupDb.get('_design/searchViews'); + } catch (error) { + console.log(error) + if (error.name === 'not_found') { + await importViews(groupDb); + } else { + return false; + } + } + const results = await groupDb.query('searchViews/responseByFormID', {include_docs: true, startkey:req.query.id,endkey: `${req.query.id}\ufff0`,}); + const docs = results.rows.map(row => row.doc) res.send(docs) } } catch (error) { log.error(error); res.status(500).send(error); } -} \ No newline at end of file +} + +export const importViews = async (db) => { + await db.put({ + _id: "_design/searchViews", + views: { + responseByFormID: { + map: function (doc) { + if (doc.collection === "TangyFormResponse") { + emit(doc._id, true); + } + }.toString(), + } + } + }); + await db.query("searchViews/responseByFormID", { limit: 0 }); +}; \ No newline at end of file From 472a9f57a109bb8dff6a481a585f6ba2a6468024 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Tue, 9 Apr 2024 11:57:09 +0300 Subject: [PATCH 22/81] fix(editor): formatting --- server/src/routes/group-responses.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index f5ea1a72e4..25d457fe21 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -36,7 +36,6 @@ module.exports = async (req, res) => { res.status(500).send(error); } } - export const importViews = async (db) => { await db.put({ _id: "_design/searchViews", @@ -51,4 +50,4 @@ export const importViews = async (db) => { } }); await db.query("searchViews/responseByFormID", { limit: 0 }); -}; \ No newline at end of file +}; From 2f86be410910dda6e58520e78456720ab083de1e Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 11 Apr 2024 09:13:35 +0300 Subject: [PATCH 23/81] chore(editor): Use allDocs for querying --- server/src/routes/group-responses.js | 29 ++-------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index 25d457fe21..4fb19a04af 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -17,17 +17,7 @@ module.exports = async (req, res) => { const docs = results.rows.map(row => row.doc) res.send(docs) } else{ - try { - await groupDb.get('_design/searchViews'); - } catch (error) { - console.log(error) - if (error.name === 'not_found') { - await importViews(groupDb); - } else { - return false; - } - } - const results = await groupDb.query('searchViews/responseByFormID', {include_docs: true, startkey:req.query.id,endkey: `${req.query.id}\ufff0`,}); + const results = await groupDb.allDocs({include_docs: true, startkey:req.query.id,endkey: `${req.query.id}\ufff0`,}); const docs = results.rows.map(row => row.doc) res.send(docs) } @@ -35,19 +25,4 @@ module.exports = async (req, res) => { log.error(error); res.status(500).send(error); } -} -export const importViews = async (db) => { - await db.put({ - _id: "_design/searchViews", - views: { - responseByFormID: { - map: function (doc) { - if (doc.collection === "TangyFormResponse") { - emit(doc._id, true); - } - }.toString(), - } - } - }); - await db.query("searchViews/responseByFormID", { limit: 0 }); -}; +} \ No newline at end of file From b41b265ce8fe29c4f15f6caffed7a69c6f79f5b5 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 11 Apr 2024 11:27:11 +0300 Subject: [PATCH 24/81] refactor(editor): Add pagination --- editor/src/app/groups/responses/responses.component.html | 2 +- editor/src/app/groups/responses/responses.component.ts | 4 ++++ server/src/routes/group-responses.js | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/editor/src/app/groups/responses/responses.component.html b/editor/src/app/groups/responses/responses.component.html index ffa3f422ce..93fa80fc0f 100644 --- a/editor/src/app/groups/responses/responses.component.html +++ b/editor/src/app/groups/responses/responses.component.html @@ -13,4 +13,4 @@ < back -next > +next > diff --git a/editor/src/app/groups/responses/responses.component.ts b/editor/src/app/groups/responses/responses.component.ts index 55d1883a6b..f246efa782 100644 --- a/editor/src/app/groups/responses/responses.component.ts +++ b/editor/src/app/groups/responses/responses.component.ts @@ -67,6 +67,8 @@ export class ResponsesComponent implements OnInit { if (searchString.length > 2) { if (this.searchString === searchString) return; this.searchResults.nativeElement.innerHTML = 'Searching...' + this.skip = 0 + this.limit = 30 this.onSearch$.next(event.target.value) } if(searchString.length <= 2 && searchString.length !==0 ) { this.searchResults.nativeElement.innerHTML = ` @@ -77,6 +79,8 @@ export class ResponsesComponent implements OnInit { } if(searchString.length===0){ this.searchResults.nativeElement.innerHTML = '' this.searchString = '' + this.skip = 0 + this.limit = 30 await this.getResponses() } }) diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index 4fb19a04af..96f5aae152 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -17,8 +17,8 @@ module.exports = async (req, res) => { const docs = results.rows.map(row => row.doc) res.send(docs) } else{ - const results = await groupDb.allDocs({include_docs: true, startkey:req.query.id,endkey: `${req.query.id}\ufff0`,}); - const docs = results.rows.map(row => row.doc) + const results = await groupDb.allDocs({include_docs: true, startkey:req.query.id,endkey: `${req.query.id}\ufff0`, skip: options.skip, limit:options.limit}); + const docs = results.rows.map(row => row.doc.collection == "TangyFormResponse" && row.doc) res.send(docs) } } catch (error) { From e390dc8f33532640b27e08738141eb4ce94764d5 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 11 Apr 2024 12:14:37 -0400 Subject: [PATCH 25/81] Refactor group-responses to use common options --- server/src/routes/group-responses.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index 96f5aae152..c838b8d61e 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -5,19 +5,25 @@ const log = require('tangy-log').log module.exports = async (req, res) => { try { const groupDb = new DB(req.params.groupId) - let options = {key: req.params.formId, include_docs: true, descending: true} + let options = {include_docs: true} if (req.params.limit) { options.limit = req.params.limit } if (req.params.skip) { options.skip = req.params.skip } - if (Object.keys(req.query).length < 1 ){ + if (Object.keys(req.query).length > 1 ) { + // get all responses for a form + options.key = req.params.formId; + options.descending = true; const results = await groupDb.query('responsesByStartUnixTime', options); const docs = results.rows.map(row => row.doc) res.send(docs) - } else{ - const results = await groupDb.allDocs({include_docs: true, startkey:req.query.id,endkey: `${req.query.id}\ufff0`, skip: options.skip, limit:options.limit}); + } else { + // searh options by document id + options.startkey = req.query.id; + options.endkey = `${req.query.id}\ufff0`; + const results = await groupDb.allDocs(options); const docs = results.rows.map(row => row.doc.collection == "TangyFormResponse" && row.doc) res.send(docs) } From 2a326712acf7495cc577874366828af647534397 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 11 Apr 2024 12:16:14 -0400 Subject: [PATCH 26/81] Refactor for readibility --- server/src/routes/group-responses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index c838b8d61e..afd439110c 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -24,7 +24,7 @@ module.exports = async (req, res) => { options.startkey = req.query.id; options.endkey = `${req.query.id}\ufff0`; const results = await groupDb.allDocs(options); - const docs = results.rows.map(row => row.doc.collection == "TangyFormResponse" && row.doc) + const docs = results.rows.map(row => row.doc && row.doc.collection == "TangyFormResponse") res.send(docs) } } catch (error) { From 9d720c7d112208b70b0dd55ff3361c74b8daadd3 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 11 Apr 2024 13:34:15 -0400 Subject: [PATCH 27/81] Fix error in map funciton --- server/src/routes/group-responses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index afd439110c..b468621070 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -24,7 +24,7 @@ module.exports = async (req, res) => { options.startkey = req.query.id; options.endkey = `${req.query.id}\ufff0`; const results = await groupDb.allDocs(options); - const docs = results.rows.map(row => row.doc && row.doc.collection == "TangyFormResponse") + const docs = results.rows.map(row => { if (row.doc && row.doc.collection == "TangyFormResponse") { return row.doc } }); res.send(docs) } } catch (error) { From d7e27f59cbc140ab14675fefd3c75bc6d52f9089 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 11 Apr 2024 13:51:47 -0400 Subject: [PATCH 28/81] Fix error in refactor code --- server/src/routes/group-responses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index b468621070..7016e65a8a 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -12,7 +12,7 @@ module.exports = async (req, res) => { if (req.params.skip) { options.skip = req.params.skip } - if (Object.keys(req.query).length > 1 ) { + if (Object.keys(req.query).length < 1 ) { // get all responses for a form options.key = req.params.formId; options.descending = true; From 2e977559554548a41b37835307ca5934bdf43f93 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 11 Apr 2024 15:54:47 -0400 Subject: [PATCH 29/81] Fix return of results for tangerine teach context --- server/src/routes/group-responses.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/src/routes/group-responses.js b/server/src/routes/group-responses.js index 7016e65a8a..ff9155f941 100644 --- a/server/src/routes/group-responses.js +++ b/server/src/routes/group-responses.js @@ -12,18 +12,19 @@ module.exports = async (req, res) => { if (req.params.skip) { options.skip = req.params.skip } + let results = undefined; if (Object.keys(req.query).length < 1 ) { // get all responses for a form options.key = req.params.formId; options.descending = true; - const results = await groupDb.query('responsesByStartUnixTime', options); - const docs = results.rows.map(row => row.doc) - res.send(docs) + results = await groupDb.query('responsesByStartUnixTime', options); } else { // searh options by document id options.startkey = req.query.id; options.endkey = `${req.query.id}\ufff0`; - const results = await groupDb.allDocs(options); + results = await groupDb.allDocs(options); + } + if (results) { const docs = results.rows.map(row => { if (row.doc && row.doc.collection == "TangyFormResponse") { return row.doc } }); res.send(docs) } From a67d9f7dc451ceb2d088239718b44bf193284dd0 Mon Sep 17 00:00:00 2001 From: esurface Date: Mon, 13 May 2024 11:29:23 -0700 Subject: [PATCH 30/81] Search service: exclude archived cases from recent activity --- client/src/app/shared/_services/search.service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/app/shared/_services/search.service.ts b/client/src/app/shared/_services/search.service.ts index b363eb220b..14a53927ca 100644 --- a/client/src/app/shared/_services/search.service.ts +++ b/client/src/app/shared/_services/search.service.ts @@ -44,7 +44,7 @@ export class SearchService { await createSearchIndex(db, formsInfo, customSearchJs) } - async search(username:string, phrase:string, limit = 50, skip = 0):Promise> { + async search(username:string, phrase:string, limit = 50, skip = 0, excludeArchived = true):Promise> { const db = await this.userService.getUserDatabase(username) let result:any = {} let activity = [] @@ -53,13 +53,18 @@ export class SearchService { } // Only show activity if they have enough activity to fill a page. if (phrase === '' && activity.length >= 11) { - const page = activity.slice(skip, skip + limit) + let page = activity.slice(skip, skip + limit) result = await db.allDocs( { keys: page, include_docs: true } ) + if (excludeArchived) { + // Filter out archived + page = page.filter(id => !result.rows.find(row => row.id === id).doc.archived); + } + // Sort it because the order of the docs returned is not guaranteed by the order of the keys parameter. result.rows = page.map(id => result.rows.find(row => row.id === id)) } else { From ac2cf76c8a7471730476322b460675597e861bd0 Mon Sep 17 00:00:00 2001 From: esurface Date: Mon, 13 May 2024 11:36:33 -0700 Subject: [PATCH 31/81] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bee29a0263..cd06b5e520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ __New Features__ - Add ID Search to Data > Import: A new feature in the Data > Import screen allows users to search for a specific ID in the imported data. This feature is useful for finding specific records synced to the server when reviewing or editing completed form responses. +__Fixes__ +- Client Search Service: exclude archived cases from recent activity + __Tangerine Teach__ - Add toggle in Attendence Check for 'late'. A teacher can click through the status of 'present', 'late', or 'absent' for each student. From 1399a4ac6a9a391472377e651feb50918a945689 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Tue, 14 May 2024 21:44:33 +0300 Subject: [PATCH 32/81] feat(import-profile): Import user profile and docs to device Download user profile and corresponding docs to device. Allow user to retry on failure or resume Refs Tangerine-Community/Tangerine#3696 --- .../import-user-profile.component.ts | 20 ++++++++++++++----- ...up-responses-by-user-profile-short-code.js | 13 +++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index a88533c083..02a4449987 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -45,12 +45,22 @@ export class ImportUserProfileComponent implements AfterContentInit { this.state = this.STATE_SYNCING this.appConfig = await this.appConfigService.getAppConfig() const shortCode = this.userShortCodeInput.nativeElement.value - this.docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}`).toPromise() - const newUserProfile = this.docs.find(doc => doc.form && doc.form.id === 'user-profile') + let docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/1/0`).toPromise() as Array + const newUserProfile = docs.find(doc => doc.form && doc.form.id === 'user-profile') await this.userService.saveUserAccount({...userAccount, userUUID: newUserProfile._id, initialProfileComplete: true}) - for (let doc of this.docs) { - delete doc._rev - await db.put(doc) + const totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/1/0`).toPromise())['totalDocs'] + const docsToQuery = 20; + let processedDocs = 0; + let index = 0; + while (processedDocs < totalDocs) { + const skipDocs = docsToQuery * index + this.docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/${docsToQuery}/${skipDocs}`).toPromise() + for (let doc of this.docs) { + delete doc._rev + await db.put(doc) + } + index++; + processedDocs += docsToQuery; } this.router.navigate([`/${this.appConfig.homeUrl}`] ); } diff --git a/server/src/routes/group-responses-by-user-profile-short-code.js b/server/src/routes/group-responses-by-user-profile-short-code.js index 2bf65f469c..2d3f7ad670 100644 --- a/server/src/routes/group-responses-by-user-profile-short-code.js +++ b/server/src/routes/group-responses-by-user-profile-short-code.js @@ -12,9 +12,16 @@ module.exports = async (req, res) => { if (req.params.skip) { options.skip = req.params.skip } - const results = await groupDb.query('responsesByUserProfileShortCode', options); - const docs = results.rows.map(row => row.doc) - res.send(docs) + if(req.query.totalRows){ + options.limit = 1 + options.skip =0 + const results = await groupDb.query('responsesByUserProfileShortCode', options); + res.send({totalRows:results.total_rows}) + }else{ + const results = await groupDb.query('responsesByUserProfileShortCode', options); + const docs = results.rows.map(row => row.doc) + res.send(docs) + } } catch (error) { log.error(error); res.status(500).send(error); From 6bf8c806cb6763ad43ff238bef1f807b9914b318 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 15 May 2024 09:50:27 +0300 Subject: [PATCH 33/81] feat(import-profile): Add retry logic Refs Tangerine-Community/Tangerine#3696 --- .../import-user-profile.component.html | 3 +- .../import-user-profile.component.ts | 67 ++++++++++++------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.html b/client/src/app/user-profile/import-user-profile/import-user-profile.component.html index 71ad6c9154..ccde48a0d5 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.html +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.html @@ -1,7 +1,8 @@

{{'submit'|translate}} + {{'Retry'|translate}}

- {{'Syncing'|translate}}... + {{'Syncing'|translate}}...{{processedDocs}} {{'of'|translate}} {{totalDocs}}

diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index 02a4449987..be5db902ea 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -20,6 +20,10 @@ export class ImportUserProfileComponent implements AfterContentInit { appConfig: AppConfig state = this.STATE_INPUT docs; + totalDocs; + processedDocs; + userAccount; + db; @ViewChild('userShortCode', {static: true}) userShortCodeInput: ElementRef; constructor( @@ -27,42 +31,53 @@ export class ImportUserProfileComponent implements AfterContentInit { private http: HttpClient, private userService: UserService, private appConfigService: AppConfigService - ) { } + ) { } ngAfterContentInit() { } async onSubmit() { const username = this.userService.getCurrentUser() - const db = await this.userService.getUserDatabase(this.userService.getCurrentUser()) - const userAccount = await this.userService.getUserAccount(this.userService.getCurrentUser()) + this.db = await this.userService.getUserDatabase(this.userService.getCurrentUser()) + this.userAccount = await this.userService.getUserAccount(this.userService.getCurrentUser()) try { - const profileToReplace = await db.get(userAccount.userUUID) - await db.remove(profileToReplace) + const profileToReplace = await this.db.get(this.userAccount.userUUID) + await this.db.remove(profileToReplace) } catch(e) { // It's ok if this fails. It's probably because they are trying again and the profile has already been deleted. } - this.state = this.STATE_SYNCING - this.appConfig = await this.appConfigService.getAppConfig() - const shortCode = this.userShortCodeInput.nativeElement.value - let docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/1/0`).toPromise() as Array - const newUserProfile = docs.find(doc => doc.form && doc.form.id === 'user-profile') - await this.userService.saveUserAccount({...userAccount, userUUID: newUserProfile._id, initialProfileComplete: true}) - const totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/1/0`).toPromise())['totalDocs'] - const docsToQuery = 20; - let processedDocs = 0; - let index = 0; - while (processedDocs < totalDocs) { - const skipDocs = docsToQuery * index - this.docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/${docsToQuery}/${skipDocs}`).toPromise() - for (let doc of this.docs) { - delete doc._rev - await db.put(doc) - } - index++; - processedDocs += docsToQuery; - } + await this.startSyncing() this.router.navigate([`/${this.appConfig.homeUrl}`] ); } -} + async onRetry(){ + await this.startSyncing() + } + + async startSyncing(){ + try { + this.state = this.STATE_SYNCING + this.appConfig = await this.appConfigService.getAppConfig() + const shortCode = this.userShortCodeInput.nativeElement.value + let docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/1/0`).toPromise() as Array + const newUserProfile = docs.find(doc => doc.form && doc.form.id === 'user-profile') + await this.userService.saveUserAccount({...this.userAccount, userUUID: newUserProfile._id, initialProfileComplete: true}) + this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/1/0`).toPromise())['totalDocs'] + const docsToQuery = 20; + let processedDocs = +localStorage.getItem('processedDocs')||0; + while (processedDocs < this.totalDocs) { + this.docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/${docsToQuery}/${processedDocs}`).toPromise() + for (let doc of this.docs) { + delete doc._rev + await this.db.put(doc) + } + processedDocs += this.docs.length; + this.processedDocs = processedDocs + localStorage.setItem('processedDocs', String(processedDocs)) + } + } catch (error) { + this.state = 'ERROR' + } + } + +} \ No newline at end of file From ba5cc342900b37cbe8517e262ebe7c35e08d1ea5 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 16 May 2024 12:44:29 -0400 Subject: [PATCH 34/81] Updates after review --- .../import-user-profile/import-user-profile.component.ts | 7 ++++--- .../routes/group-responses-by-user-profile-short-code.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index be5db902ea..a5c0004f76 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -17,6 +17,7 @@ export class ImportUserProfileComponent implements AfterContentInit { STATE_SYNCING = 'STATE_SYNCING' STATE_INPUT = 'STATE_INPUT' + STATE_ERROR = 'STATE_ERROR' appConfig: AppConfig state = this.STATE_INPUT docs; @@ -38,8 +39,8 @@ export class ImportUserProfileComponent implements AfterContentInit { async onSubmit() { const username = this.userService.getCurrentUser() - this.db = await this.userService.getUserDatabase(this.userService.getCurrentUser()) - this.userAccount = await this.userService.getUserAccount(this.userService.getCurrentUser()) + this.db = await this.userService.getUserDatabase(username) + this.userAccount = await this.userService.getUserAccount(username) try { const profileToReplace = await this.db.get(this.userAccount.userUUID) await this.db.remove(profileToReplace) @@ -76,7 +77,7 @@ export class ImportUserProfileComponent implements AfterContentInit { localStorage.setItem('processedDocs', String(processedDocs)) } } catch (error) { - this.state = 'ERROR' + this.state = this.STATE_ERROR } } diff --git a/server/src/routes/group-responses-by-user-profile-short-code.js b/server/src/routes/group-responses-by-user-profile-short-code.js index 2d3f7ad670..9439bbba0e 100644 --- a/server/src/routes/group-responses-by-user-profile-short-code.js +++ b/server/src/routes/group-responses-by-user-profile-short-code.js @@ -14,7 +14,7 @@ module.exports = async (req, res) => { } if(req.query.totalRows){ options.limit = 1 - options.skip =0 + options.skip = 0 const results = await groupDb.query('responsesByUserProfileShortCode', options); res.send({totalRows:results.total_rows}) }else{ From dd8a73d93706b22902e71134e5cae3b87471192a Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 23 May 2024 21:16:12 -0400 Subject: [PATCH 35/81] Add attendence, behavior, and score to mysql outputs --- server/src/modules/mysql-js/index.js | 51 +++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/server/src/modules/mysql-js/index.js b/server/src/modules/mysql-js/index.js index 06f7e5c027..6dc45e6c76 100644 --- a/server/src/modules/mysql-js/index.js +++ b/server/src/modules/mysql-js/index.js @@ -206,7 +206,7 @@ module.exports = { } const result = await saveToMysql(knex, sourceDb,flatDoc, tablenameSuffix, tableName, docType, primaryKey, createFunction) log.info('Processed: ' + JSON.stringify(result)) - } else { + } else if (doc.type === 'response') { const flatDoc = await prepareFlatData(doc, sanitized); tableName = null; docType = 'response'; @@ -222,7 +222,25 @@ module.exports = { } const result = await saveToMysql(knex, sourceDb,flatDoc, tablenameSuffix, tableName, docType, primaryKey, createFunction) log.info('Processed: ' + JSON.stringify(result)) + } else { + const flatDoc = await prepareFlatData(doc, sanitized); + tableName = flatDoc.type; + console.log("tableName: " + tableName) + docType = 'response'; + primaryKey = 'ID' + createFunction = function (t) { + t.engine('InnoDB') + t.string(primaryKey, 200).notNullable().primary(); + t.string('caseId', 36) // .index('response_caseId_IDX'); + t.string('participantID', 36) //.index('case_instances_ParticipantID_IDX'); + t.string('caseEventId', 36) // .index('eventform_caseEventId_IDX'); + t.tinyint('complete'); + t.string('archived', 36); // TODO: "sqlMessage":"Incorrect integer value: '' for column 'archived' at row 1 + } + const result = await saveToMysql(knex, sourceDb,flatDoc, tablenameSuffix, tableName, docType, primaryKey, createFunction) + log.info('Processed: ' + JSON.stringify(result)) } + await knex.destroy() } } @@ -326,9 +344,36 @@ const generateFlatResponse = async function (formResponse, sanitized) { if (formResponse.form.id === '') { formResponse.form.id = 'blank' } + console.log('formResponse.form.id: ' + formResponse.form.id) flatFormResponse['formId'] = formResponse.form.id } + if (formResponse.type === 'attendance' || formResponse.type === 'behavior' || formResponse.type === 'scores') { + if (formResponse.type === 'attendance') { + flatFormResponse['attendanceList'] = formResponse.attendanceList + } else if (formResponse.type === 'behavior') { + // flatFormResponse['studentBehaviorList'] = formResponse.studentBehaviorList + const studentBehaviorList = formResponse.studentBehaviorList.map(record => { + const student = {} + Object.keys(record).forEach(key => { + if (key === 'behavior') { + student[key + '.formResponseId'] = record[key]['formResponseId'] + student[key + '.internal'] = record[key]['internal'] + student[key + '.internalPercentage'] = record[key]['internalPercentage'] + // console.log("special processing for behavior: " + JSON.stringify(student) ) + } else { + // console.log("key: " + key + " record[key]: " + record[key]) + student[key] = record[key] + } + }) + return student + }) + flatFormResponse['studentBehaviorList'] = studentBehaviorList + } else if (formResponse.type === 'scores') { + flatFormResponse['scoreList'] = formResponse.scoreList + } + } + for (let item of formResponse.items) { for (let input of item.inputs) { let sanitize = false; @@ -661,6 +706,7 @@ async function convert_response(knex, doc, groupId, tableName) { const caseEventId = doc.caseEventId var formID = doc.formId || data['formid'] + console.log("formID: " + formID) if (formID) { // thanks to https://stackoverflow.com/a/14822579 const find = '-' @@ -817,6 +863,9 @@ async function saveToMysql(knex, sourceDb, doc, tablenameSuffix, tableName, docT data = await convert_issue(knex, doc, groupId, tableName) break; case 'response': + case 'attendance': + case 'behavior': + case 'scores': data = await convert_response(knex, doc, groupId, tableName) // Check if table exists and create if needed: tableName = data['formID_sanitized'] + tablenameSuffix From e0cca4873a63db910c638a62590052d032c9eb23 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 23 May 2024 21:21:31 -0400 Subject: [PATCH 36/81] Update tangy-form to v4.43.2 --- client/package.json | 2 +- editor/package.json | 2 +- online-survey-app/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/package.json b/client/package.json index a8d85a4531..a87023f962 100644 --- a/client/package.json +++ b/client/package.json @@ -75,7 +75,7 @@ "rxjs-compat": "^6.5.5", "sanitize-filename-ts": "^1.0.2", "spark-md5": "^3.0.2", - "tangy-form": "^4.43.2-rc.3", + "tangy-form": "^4.43.2", "translation-web-component": "1.1.1", "tslib": "^1.10.0", "underscore": "^1.9.1", diff --git a/editor/package.json b/editor/package.json index cc58642305..96ef3a359c 100644 --- a/editor/package.json +++ b/editor/package.json @@ -49,7 +49,7 @@ "redux": "^4.0.5", "rxjs": "~6.5.5", "rxjs-compat": "^6.5.5", - "tangy-form": "^4.43.2-rc.3", + "tangy-form": "^4.43.2", "tangy-form-editor": "7.18.0", "translation-web-component": "0.0.3", "tslib": "^1.11.2", diff --git a/online-survey-app/package.json b/online-survey-app/package.json index 3792ce7c22..a958912eb8 100644 --- a/online-survey-app/package.json +++ b/online-survey-app/package.json @@ -25,7 +25,7 @@ "material-design-icons-iconfont": "^6.1.0", "redux": "^4.0.5", "rxjs": "~6.6.0", - "tangy-form": "^4.43.2-rc.3", + "tangy-form": "^4.43.2", "translation-web-component": "^1.1.0", "tslib": "^2.0.0", "zone.js": "~0.10.2" From 38567f744056771345e528cfb060bbd753451f16 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 23 May 2024 21:26:32 -0400 Subject: [PATCH 37/81] Update tangerine-preview version to v3.31.0 --- tangerine-preview/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangerine-preview/package.json b/tangerine-preview/package.json index f00c17c64f..d99cae3438 100644 --- a/tangerine-preview/package.json +++ b/tangerine-preview/package.json @@ -1,6 +1,6 @@ { "name": "tangerine-preview", - "version": "3.31.0-rc-8", + "version": "3.31.0", "description": "", "main": "index.js", "bin": { From f70e5de40884dcf19f64e89b1fed081b07841776 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 23 May 2024 17:42:29 +0300 Subject: [PATCH 38/81] build(import-profile): Add views and remove redundant retry button Refs Tangerine-Community/Tangerine#3696 --- .../import-user-profile.component.html | 1 - .../import-user-profile.component.ts | 10 +++------- server/src/group-views.js | 7 +++++++ .../group-responses-by-user-profile-short-code.js | 15 +++++++++++---- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.html b/client/src/app/user-profile/import-user-profile/import-user-profile.component.html index ccde48a0d5..17e43ee077 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.html +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.html @@ -1,7 +1,6 @@

{{'submit'|translate}} - {{'Retry'|translate}}

{{'Syncing'|translate}}...{{processedDocs}} {{'of'|translate}} {{totalDocs}} diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index a5c0004f76..a551200fe7 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -51,19 +51,15 @@ export class ImportUserProfileComponent implements AfterContentInit { this.router.navigate([`/${this.appConfig.homeUrl}`] ); } - async onRetry(){ - await this.startSyncing() - } - async startSyncing(){ try { this.state = this.STATE_SYNCING this.appConfig = await this.appConfigService.getAppConfig() - const shortCode = this.userShortCodeInput.nativeElement.value - let docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/1/0`).toPromise() as Array + const shortCode = this.userShortCodeInput.nativeElement.value; + let docs = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/?userProfile=true`).toPromise() as Array const newUserProfile = docs.find(doc => doc.form && doc.form.id === 'user-profile') await this.userService.saveUserAccount({...this.userAccount, userUUID: newUserProfile._id, initialProfileComplete: true}) - this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/1/0`).toPromise())['totalDocs'] + this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/?totalRows=true`).toPromise())['totalDocs'] const docsToQuery = 20; let processedDocs = +localStorage.getItem('processedDocs')||0; while (processedDocs < this.totalDocs) { diff --git a/server/src/group-views.js b/server/src/group-views.js index 5d9690cb3c..75ca6f0379 100644 --- a/server/src/group-views.js +++ b/server/src/group-views.js @@ -77,6 +77,13 @@ module.exports.responsesByUserProfileShortCode = function(doc) { } } +module.exports.userProfileByUserProfileShortCode = function(doc) { + if (doc.collection === "TangyFormResponse") { + if (doc.form && doc.form.id === 'user-profile') { + return emit(doc._id.substr(doc._id.length-6, doc._id.length), true) + } + } +} module.exports.groupIssues = function(doc) { if (doc.collection === "TangyFormResponse" && doc.type === "issue") { var lastFilledOutNode; diff --git a/server/src/routes/group-responses-by-user-profile-short-code.js b/server/src/routes/group-responses-by-user-profile-short-code.js index 9439bbba0e..d1a91d4d8e 100644 --- a/server/src/routes/group-responses-by-user-profile-short-code.js +++ b/server/src/routes/group-responses-by-user-profile-short-code.js @@ -5,24 +5,31 @@ const log = require('tangy-log').log module.exports = async (req, res) => { try { const groupDb = new DB(req.params.groupId) - let options = {key: req.params.userProfileShortCode, include_docs: true} + const userProfileShortCode = req.params.userProfileShortCode + let options = { key: userProfileShortCode, include_docs: true } if (req.params.limit) { options.limit = req.params.limit } if (req.params.skip) { options.skip = req.params.skip } - if(req.query.totalRows){ + if (req.query.totalRows) { options.limit = 1 options.skip = 0 const results = await groupDb.query('responsesByUserProfileShortCode', options); - res.send({totalRows:results.total_rows}) - }else{ + res.send({ totalRows: results.total_rows }) + } else if (req.query.userProfile) { + + await groupDb.query("userProfileByUserProfileShortCode", { limit: 0 }); + const profile = await groupDb.query("userProfileByUserProfileShortCode", { key: userProfileShortCode, limit: 1, include_docs: true }); + res.send(profile.rows[0]) + } else { const results = await groupDb.query('responsesByUserProfileShortCode', options); const docs = results.rows.map(row => row.doc) res.send(docs) } } catch (error) { + console.log(error) log.error(error); res.status(500).send(error); } From b3923d35d5159c8c4e7f2f273de7721dfe1e14a9 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Fri, 24 May 2024 12:41:11 +0300 Subject: [PATCH 39/81] build(import-user-profile): Increase docs to query per call and improve UI - increase docs per call to 1000 - syncing state should only be set when the short-code is found in the remote db - Send minimal profile data to the client Refs Tangerine-Community/Tangerine#ticket number --- .../import-user-profile.component.ts | 31 ++++++++++--------- server/src/group-views.js | 11 +++---- ...up-responses-by-user-profile-short-code.js | 9 +++--- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index a551200fe7..be6cb3f2f2 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -53,24 +53,25 @@ export class ImportUserProfileComponent implements AfterContentInit { async startSyncing(){ try { - this.state = this.STATE_SYNCING this.appConfig = await this.appConfigService.getAppConfig() const shortCode = this.userShortCodeInput.nativeElement.value; - let docs = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/?userProfile=true`).toPromise() as Array - const newUserProfile = docs.find(doc => doc.form && doc.form.id === 'user-profile') - await this.userService.saveUserAccount({...this.userAccount, userUUID: newUserProfile._id, initialProfileComplete: true}) - this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/?totalRows=true`).toPromise())['totalDocs'] - const docsToQuery = 20; - let processedDocs = +localStorage.getItem('processedDocs')||0; - while (processedDocs < this.totalDocs) { - this.docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/${docsToQuery}/${processedDocs}`).toPromise() - for (let doc of this.docs) { - delete doc._rev - await this.db.put(doc) + let newUserProfile = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/?userProfile=true`).toPromise() + if(!!newUserProfile){ + this.state = this.STATE_SYNCING + await this.userService.saveUserAccount({ ...this.userAccount, userUUID: newUserProfile['_id'], initialProfileComplete: true }) + this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/?totalRows=true`).toPromise())['totalDocs'] + const docsToQuery = 1000; + let processedDocs = +localStorage.getItem('processedDocs') || 0; + while (processedDocs < this.totalDocs) { + this.docs = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/${docsToQuery}/${processedDocs}`).toPromise() + for (let doc of this.docs) { + delete doc._rev + await this.db.put(doc) + } + processedDocs += this.docs.length; + this.processedDocs = processedDocs + localStorage.setItem('processedDocs', String(processedDocs)) } - processedDocs += this.docs.length; - this.processedDocs = processedDocs - localStorage.setItem('processedDocs', String(processedDocs)) } } catch (error) { this.state = this.STATE_ERROR diff --git a/server/src/group-views.js b/server/src/group-views.js index 75ca6f0379..1d8e7725f3 100644 --- a/server/src/group-views.js +++ b/server/src/group-views.js @@ -77,13 +77,12 @@ module.exports.responsesByUserProfileShortCode = function(doc) { } } -module.exports.userProfileByUserProfileShortCode = function(doc) { - if (doc.collection === "TangyFormResponse") { - if (doc.form && doc.form.id === 'user-profile') { - return emit(doc._id.substr(doc._id.length-6, doc._id.length), true) - } +module.exports.userProfileByUserProfileShortCode = function (doc) { + if (doc.collection === "TangyFormResponse"&&doc.form && doc.form.id === 'user-profile') { + return emit(doc._id.substr(doc._id.length - 6, doc._id.length), true); } } + module.exports.groupIssues = function(doc) { if (doc.collection === "TangyFormResponse" && doc.type === "issue") { var lastFilledOutNode; @@ -221,4 +220,4 @@ module.exports.byConflictDocId = { emit(doc.conflictDocId, doc.conflictRev); }.toString(), reduce: '_count' -} \ No newline at end of file +} diff --git a/server/src/routes/group-responses-by-user-profile-short-code.js b/server/src/routes/group-responses-by-user-profile-short-code.js index d1a91d4d8e..9f397bef56 100644 --- a/server/src/routes/group-responses-by-user-profile-short-code.js +++ b/server/src/routes/group-responses-by-user-profile-short-code.js @@ -17,12 +17,13 @@ module.exports = async (req, res) => { options.limit = 1 options.skip = 0 const results = await groupDb.query('responsesByUserProfileShortCode', options); - res.send({ totalRows: results.total_rows }) + res.send({ totalDocs: results.total_rows }) } else if (req.query.userProfile) { - await groupDb.query("userProfileByUserProfileShortCode", { limit: 0 }); - const profile = await groupDb.query("userProfileByUserProfileShortCode", { key: userProfileShortCode, limit: 1, include_docs: true }); - res.send(profile.rows[0]) + const result = await groupDb.query("userProfileByUserProfileShortCode", { key: userProfileShortCode, limit: 1, include_docs: true }); + const profile = result.rows[0] + const data = profile ? {_id: profile.id, key: profile.id, formId: profile.doc.form.id, collection: profile.doc.collection}: undefined + res.send(data) } else { const results = await groupDb.query('responsesByUserProfileShortCode', options); const docs = results.rows.map(row => row.doc) From 5c586c22e96609a91df27565cf00fc5672888f22 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Fri, 24 May 2024 12:55:42 +0300 Subject: [PATCH 40/81] build(import-profile): Update sync text and navigation - Sync text should reflect that the docs are already synced - Navigation should be the very last item after successful sync Refs Tangerine-Community/Tangerine#ticket number --- .../import-user-profile/import-user-profile.component.html | 2 +- .../import-user-profile/import-user-profile.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.html b/client/src/app/user-profile/import-user-profile/import-user-profile.component.html index 17e43ee077..a2bd3b50c4 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.html +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.html @@ -3,5 +3,5 @@ {{'submit'|translate}}

- {{'Syncing'|translate}}...{{processedDocs}} {{'of'|translate}} {{totalDocs}} + {{'Synced'|translate}} {{processedDocs}} {{'of'|translate}} {{totalDocs}}

diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index be6cb3f2f2..f3d3ac5c25 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -48,7 +48,6 @@ export class ImportUserProfileComponent implements AfterContentInit { // It's ok if this fails. It's probably because they are trying again and the profile has already been deleted. } await this.startSyncing() - this.router.navigate([`/${this.appConfig.homeUrl}`] ); } async startSyncing(){ @@ -73,6 +72,7 @@ export class ImportUserProfileComponent implements AfterContentInit { localStorage.setItem('processedDocs', String(processedDocs)) } } + this.router.navigate([`/${this.appConfig.homeUrl}`] ); } catch (error) { this.state = this.STATE_ERROR } From 6d5a30236b771f3c6f093bcd6454151972565901 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Fri, 24 May 2024 16:22:26 +0300 Subject: [PATCH 41/81] build(import-user-profile): Add retry logic and error messages - Add error messages and retry logic - Notify user when record with the supplied import code is not found Refs Tangerine-Community/Tangerine#3696 --- .../import-user-profile.component.css | 5 ++++- .../import-user-profile.component.html | 6 ++++++ .../import-user-profile.component.ts | 14 +++++++++----- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.css b/client/src/app/user-profile/import-user-profile/import-user-profile.component.css index 3383624c0b..14a4ec4f68 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.css +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.css @@ -5,4 +5,7 @@ } paper-input { margin: 15px; -} \ No newline at end of file +} +#err { + color: red; + } \ No newline at end of file diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.html b/client/src/app/user-profile/import-user-profile/import-user-profile.component.html index a2bd3b50c4..0841d00108 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.html +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.html @@ -5,3 +5,9 @@

{{'Synced'|translate}} {{processedDocs}} {{'of'|translate}} {{totalDocs}}

+

+ {{'Record with import code'|translate}} {{shortCode}} {{'not found.'|translate}} +

+

+ {{'Import not successful. Click Submit to retry.' |translate}} +

diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index f3d3ac5c25..971a5aeba2 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -18,13 +18,15 @@ export class ImportUserProfileComponent implements AfterContentInit { STATE_SYNCING = 'STATE_SYNCING' STATE_INPUT = 'STATE_INPUT' STATE_ERROR = 'STATE_ERROR' + STATE_NOT_FOUND ='STATE_NOT_FOUND' appConfig: AppConfig state = this.STATE_INPUT docs; totalDocs; - processedDocs; + processedDocs = 0; userAccount; db; + shortCode @ViewChild('userShortCode', {static: true}) userShortCodeInput: ElementRef; constructor( @@ -53,16 +55,16 @@ export class ImportUserProfileComponent implements AfterContentInit { async startSyncing(){ try { this.appConfig = await this.appConfigService.getAppConfig() - const shortCode = this.userShortCodeInput.nativeElement.value; - let newUserProfile = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/?userProfile=true`).toPromise() + this.shortCode = this.userShortCodeInput.nativeElement.value; + let newUserProfile = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?userProfile=true`).toPromise() if(!!newUserProfile){ this.state = this.STATE_SYNCING await this.userService.saveUserAccount({ ...this.userAccount, userUUID: newUserProfile['_id'], initialProfileComplete: true }) - this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/?totalRows=true`).toPromise())['totalDocs'] + this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?totalRows=true`).toPromise())['totalDocs'] const docsToQuery = 1000; let processedDocs = +localStorage.getItem('processedDocs') || 0; while (processedDocs < this.totalDocs) { - this.docs = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${shortCode}/${docsToQuery}/${processedDocs}`).toPromise() + this.docs = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/${docsToQuery}/${processedDocs}`).toPromise() for (let doc of this.docs) { delete doc._rev await this.db.put(doc) @@ -71,6 +73,8 @@ export class ImportUserProfileComponent implements AfterContentInit { this.processedDocs = processedDocs localStorage.setItem('processedDocs', String(processedDocs)) } + } else{ + this.state = this.STATE_NOT_FOUND } this.router.navigate([`/${this.appConfig.homeUrl}`] ); } catch (error) { From 56807dff7e1b4d117cbf700e87b386ce7118296a Mon Sep 17 00:00:00 2001 From: esurface Date: Fri, 24 May 2024 16:05:10 -0400 Subject: [PATCH 42/81] Add hack function to delete userProfileId if it is duplicated --- server/src/modules/mysql-js/index.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/src/modules/mysql-js/index.js b/server/src/modules/mysql-js/index.js index 6dc45e6c76..abf5c8d7d0 100644 --- a/server/src/modules/mysql-js/index.js +++ b/server/src/modules/mysql-js/index.js @@ -349,6 +349,24 @@ const generateFlatResponse = async function (formResponse, sanitized) { } if (formResponse.type === 'attendance' || formResponse.type === 'behavior' || formResponse.type === 'scores') { + + function hackFunctionToRemoveUserProfileId (formResponse) { + // This is a very special hack function to remove userProfileId + // It needs to be replaced with a proper solution that resolves duplicate variables. + if (formResponse.userProfileId) { + for (let item of formResponse.items) { + for (let input of item.inputs) { + if (input.name === 'userProfileId') { + delete formResponse.userProfileId; + } + } + } + } + return formResponse; + } + + formResponse = hackFunctionToRemoveUserProfileId(formResponse); + if (formResponse.type === 'attendance') { flatFormResponse['attendanceList'] = formResponse.attendanceList } else if (formResponse.type === 'behavior') { From b6402f11fbc8724adb90dd8e9a034e36943f645d Mon Sep 17 00:00:00 2001 From: esurface Date: Tue, 28 May 2024 12:00:27 -0400 Subject: [PATCH 43/81] Apply hack functiomn to student-registration and class-registration forms --- server/src/modules/mysql-js/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/modules/mysql-js/index.js b/server/src/modules/mysql-js/index.js index abf5c8d7d0..587431255c 100644 --- a/server/src/modules/mysql-js/index.js +++ b/server/src/modules/mysql-js/index.js @@ -213,7 +213,7 @@ module.exports = { primaryKey = 'ID' createFunction = function (t) { t.engine('InnoDB') - t.string(primaryKey, 36).notNullable().primary(); + t.string(primaryKey, 200).notNullable().primary(); t.string('caseId', 36) // .index('response_caseId_IDX'); t.string('participantID', 36) //.index('case_instances_ParticipantID_IDX'); t.string('caseEventId', 36) // .index('eventform_caseEventId_IDX'); @@ -348,7 +348,9 @@ const generateFlatResponse = async function (formResponse, sanitized) { flatFormResponse['formId'] = formResponse.form.id } - if (formResponse.type === 'attendance' || formResponse.type === 'behavior' || formResponse.type === 'scores') { + if (formResponse.type === 'attendance' || formResponse.type === 'behavior' || formResponse.type === 'scores' || + formResponse.form.id === 'student-registration' || + formResponse.form.id === 'class-registration') { function hackFunctionToRemoveUserProfileId (formResponse) { // This is a very special hack function to remove userProfileId From 863bbd137ae62b65ea1cccc1bf9d3b2f07c258ea Mon Sep 17 00:00:00 2001 From: esurface Date: Mon, 3 Jun 2024 11:48:45 -0400 Subject: [PATCH 44/81] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd06b5e520..17d67f3dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,17 +3,19 @@ ## v3.31.0 __New Features__ -- A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in [tangy-forms](https://github.com/Tangerine-Community/tangy-form/blob/master/CHANGELOG.md#v4430) +- A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in [tangy-forms](https://github.com/Tangerine-Community/tangy-form/blob/master/CHANGELOG.md#v4430). [#3473](https://github.com/Tangerine-Community/Tangerine/issues/3473) - Client Login Screen Custom HTML: A new app-config.json setting, `customLoginMarkup`, allows for custom HTML to be added to the login screen. This feature is useful for adding custom branding or additional information to the login screen. As an example: ```json "customLoginMarkup": "
logo
" ``` -- Add ID Search to Data > Import: A new feature in the Data > Import screen allows users to search for a specific ID in the imported data. This feature is useful for finding specific records synced to the server when reviewing or editing completed form responses. +- Add ID Search to Data > Import: A new feature in the Data > Import screen allows users to search for a specific ID in the imported data. This feature is useful for finding specific records synced to the server when reviewing or editing completed form responses. [#3681](https://github.com/Tangerine-Community/Tangerine/issues/3681) __Fixes__ - Client Search Service: exclude archived cases from recent activity +- Improve import of responses using user-profile short code on client [#3696](https://github.com/Tangerine-Community/Tangerine/issues/3696) +- Media library cannot upload photos [#3583](https://github.com/Tangerine-Community/Tangerine/issues/3583) __Tangerine Teach__ From 871132a82c07ef97e0da71a2e2ddac3d3e4ecb66 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 5 Jun 2024 13:31:38 +0300 Subject: [PATCH 45/81] feat(group-uploads-view): Add Sticky buttons when viewing a specific group upload Add three buttons - Verify - Edit - Delete These buttons float when scrolling on a page Refs Tangerine-Community/Tangerine#3703 --- .../group-uploads-view.component.css | 12 ++++++++++++ .../group-uploads-view.component.html | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.css b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.css index e69de29bb2..9c05abd7b5 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.css +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.css @@ -0,0 +1,12 @@ +.sticky { + position: sticky; + top: 20px; + max-width: fit-content; + margin-inline: auto; + z-index: 999; +} + +button, a{ + margin-right: 10px; + margin-top: 10px; +} \ No newline at end of file diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html index ba6d57d67a..66eddf3ac8 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html @@ -1,2 +1,9 @@ +
+ + + {{'Edit'|translate}} + + +
\ No newline at end of file From 24a4ddb2a912f6d2f8fd96afa828caa17ffeb5df Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 5 Jun 2024 13:35:12 +0300 Subject: [PATCH 46/81] feat(group-uploads-view): Lock form responses when in uploads view - Lock form responses when in uploads view - Allow users to delete response when viewing upload - Add groups edit component - Refs Tangerine-Community/Tangerine#3703 --- .../group-uploads-edit.component.css | 0 .../group-uploads-edit.component.html | 2 + .../group-uploads-edit.component.spec.ts | 25 ++++++++++ .../group-uploads-edit.component.ts | 49 +++++++++++++++++++ .../group-uploads-view.component.ts | 22 +++++++-- .../src/app/groups/groups-routing.module.ts | 2 + editor/src/app/groups/groups.module.ts | 4 +- .../groups/responses/responses.component.ts | 8 --- 8 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.css create mode 100644 editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.html create mode 100644 editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.spec.ts create mode 100644 editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.ts diff --git a/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.css b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.html b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.html new file mode 100644 index 0000000000..ba6d57d67a --- /dev/null +++ b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.spec.ts b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.spec.ts new file mode 100644 index 0000000000..e905d93244 --- /dev/null +++ b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupUploadsEditComponent } from './group-uploads-edit.component'; + +describe('GroupUploadsEditComponent', () => { + let component: GroupUploadsEditComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GroupUploadsEditComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupUploadsEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.ts b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.ts new file mode 100644 index 0000000000..a524475a15 --- /dev/null +++ b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.ts @@ -0,0 +1,49 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Breadcrumb } from 'src/app/shared/_components/breadcrumb/breadcrumb.component'; +import { _TRANSLATE } from 'src/app/shared/translation-marker'; +import { TangyFormsPlayerComponent } from 'src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component'; + +@Component({ + selector: 'app-group-uploads-edit', + templateUrl: './group-uploads-edit.component.html', + styleUrls: ['./group-uploads-edit.component.css'] +}) +export class GroupUploadsEditComponent implements OnInit { + + title = _TRANSLATE('Edit Upload') + breadcrumbs:Array = [] + @ViewChild('formPlayer', {static: true}) formPlayer: TangyFormsPlayerComponent + + constructor( + private route:ActivatedRoute, + private router:Router + ) { } + + ngOnInit() { + this.route.params.subscribe(params => { + this.breadcrumbs = [ + { + label: _TRANSLATE('Uploads'), + url: 'uploads' + }, + { + label: _TRANSLATE('View Upload'), + url: `uploads/${params.responseId}` + }, + { + label: this.title, + url: `uploads/${params.responseId}/edit` + } + ] + this.formPlayer.formResponseId = params.responseId + this.formPlayer.unlockFormResponses = true + this.formPlayer.render() + this.formPlayer.$submit.subscribe(async () => { + this.formPlayer.saveResponse(this.formPlayer.formEl.store.getState()) + this.router.navigate([`../`], { relativeTo: this.route }) + }) + }) + } + +} diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts index 430edb0f95..6d4f3914fd 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Breadcrumb } from './../../shared/_components/breadcrumb/breadcrumb.component'; import { _TRANSLATE } from 'src/app/shared/_services/translation-marker'; import { Component, OnInit, ViewChild } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-group-uploads-view', @@ -14,14 +15,19 @@ export class GroupUploadsViewComponent implements OnInit { title = _TRANSLATE('View Upload') breadcrumbs:Array = [] @ViewChild('formPlayer', {static: true}) formPlayer: TangyFormsPlayerComponent + responseId + groupId constructor( private route:ActivatedRoute, - private router:Router + private router:Router, + private http: HttpClient, ) { } ngOnInit() { this.route.params.subscribe(params => { + this.responseId = params.responseId + this.groupId = params.groupId this.breadcrumbs = [ { label: _TRANSLATE('Uploads'), @@ -29,11 +35,10 @@ export class GroupUploadsViewComponent implements OnInit { }, { label: this.title, - url: `uploads/view/${params.responseId}` + url: `uploads/${params.responseId}` } ] this.formPlayer.formResponseId = params.responseId - this.formPlayer.unlockFormResponses = true this.formPlayer.render() this.formPlayer.$submit.subscribe(async () => { this.formPlayer.saveResponse(this.formPlayer.formEl.store.getState()) @@ -42,4 +47,15 @@ export class GroupUploadsViewComponent implements OnInit { }) } + async delete(){ + if(confirm('Are you sure you want to delete this form response?')) { + await this.http.delete(`/api/${this.groupId}/${this.responseId}`).toPromise() + this.router.navigate([`../`], { relativeTo: this.route }) + } + } + + async verify(){ + + } + } diff --git a/editor/src/app/groups/groups-routing.module.ts b/editor/src/app/groups/groups-routing.module.ts index c63abca919..222e20421d 100644 --- a/editor/src/app/groups/groups-routing.module.ts +++ b/editor/src/app/groups/groups-routing.module.ts @@ -70,6 +70,7 @@ import { GroupCsvTemplatesComponent } from './group-csv-templates/group-csv-temp import { GroupDatabaseConflictsComponent } from './group-database-conflicts/group-database-conflicts.component'; import { DownloadStatisticalFileComponent } from './download-statistical-file/download-statistical-file.component'; import { GroupDevicePasswordPolicyComponent } from './group-device-password-policy/group-device-password-policy.component'; +import { GroupUploadsEditComponent } from './group-uploads-edit/group-uploads-edit.component'; const groupsRoutes: Routes = [ // { path: 'projects', component: GroupsComponent }, @@ -86,6 +87,7 @@ const groupsRoutes: Routes = [ { path: 'groups/:groupId/data', component: GroupDataComponent, canActivate: [LoginGuard] }, { path: 'groups/:groupId/data/uploads', component: GroupUploadsComponent, canActivate: [LoginGuard] }, { path: 'groups/:groupId/data/uploads/:responseId', component: GroupUploadsViewComponent, canActivate: [LoginGuard] }, + { path: 'groups/:groupId/data/uploads/:responseId/edit', component: GroupUploadsEditComponent, canActivate: [LoginGuard] }, { path: 'groups/:groupId/data/download-csv', component: GroupFormsCsvComponent, canActivate: [LoginGuard] }, { path: 'groups/:groupId/data/database-conflicts', component: GroupDatabaseConflictsComponent, canActivate: [LoginGuard] }, { path: 'groups/:groupId/data/csv-templates', component: GroupCsvTemplatesComponent, canActivate: [LoginGuard] }, diff --git a/editor/src/app/groups/groups.module.ts b/editor/src/app/groups/groups.module.ts index 9d6b54c97a..24511c1246 100644 --- a/editor/src/app/groups/groups.module.ts +++ b/editor/src/app/groups/groups.module.ts @@ -99,6 +99,7 @@ import { DownloadStatisticalFileComponent } from './download-statistical-file/do import { GroupDevicePasswordPolicyComponent } from './group-device-password-policy/group-device-password-policy.component'; import { GroupLocationListsComponent } from './group-location-lists/group-location-lists.component'; import { GroupLocationListNewComponent } from './group-location-list-new/group-location-list-new.component'; +import { GroupUploadsEditComponent } from './group-uploads-edit/group-uploads-edit.component'; @NgModule({ @@ -204,7 +205,8 @@ import { GroupLocationListNewComponent } from './group-location-list-new/group-l GroupCsvTemplatesComponent, GroupDatabaseConflictsComponent, DownloadStatisticalFileComponent, - GroupDevicePasswordPolicyComponent + GroupDevicePasswordPolicyComponent, + GroupUploadsEditComponent ], providers: [GroupsService, FilesService, TangerineFormsService, GroupDevicesService, TangyFormService ], }) diff --git a/editor/src/app/groups/responses/responses.component.ts b/editor/src/app/groups/responses/responses.component.ts index f246efa782..53a2a687ea 100644 --- a/editor/src/app/groups/responses/responses.component.ts +++ b/editor/src/app/groups/responses/responses.component.ts @@ -128,14 +128,6 @@ export class ResponsesComponent implements OnInit { this.skip = 0; this.getResponses(); } - - async deleteResponse(id) { - if(confirm('Are you sure you want to delete this form response?')) { - await this.http.delete(`/api/${this.groupId}/${id}`).toPromise() - this.getResponses() - } - } - nextPage() { this.skip = this.skip + this.limit if(this.searchString){ From d35e8ff4ee3ba36dbf9096fa394d6485733f11a0 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 5 Jun 2024 13:41:37 +0300 Subject: [PATCH 47/81] feat(app-dynamic-table): Add on row click to dynamic table Refs Tangerine-Community/Tangerine#3703 --- editor/src/app/groups/responses/responses.component.html | 2 +- editor/src/app/groups/responses/responses.component.ts | 6 ++---- .../_components/dynamic-table/dynamic-table.component.css | 3 +++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/editor/src/app/groups/responses/responses.component.html b/editor/src/app/groups/responses/responses.component.html index 93fa80fc0f..362e34900f 100644 --- a/editor/src/app/groups/responses/responses.component.html +++ b/editor/src/app/groups/responses/responses.component.html @@ -10,7 +10,7 @@
- +
< back next > diff --git a/editor/src/app/groups/responses/responses.component.ts b/editor/src/app/groups/responses/responses.component.ts index 53a2a687ea..a3df81ea5b 100644 --- a/editor/src/app/groups/responses/responses.component.ts +++ b/editor/src/app/groups/responses/responses.component.ts @@ -149,9 +149,7 @@ export class ResponsesComponent implements OnInit { onRowEdit(row) { this.router.navigate([row._id ? row._id : row.id], {relativeTo: this.route}) } - - onRowDelete(row) { - this.deleteResponse(row._id ? row._id : row.id) + onRowClick(row){ + this.router.navigate([row._id ? row._id : row.id], {relativeTo: this.route}) } - } diff --git a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.css b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.css index 0081f73aa7..22016e53ea 100644 --- a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.css +++ b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.css @@ -36,3 +36,6 @@ mat-cell, mat-header-cell { .mat-header-cell, .mat-cell { padding-left: 5px; } +.mat-row{ + cursor: pointer; +} \ No newline at end of file From d4867855f773b255aaf208530d35400d03a5b13d Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 5 Jun 2024 15:25:57 +0300 Subject: [PATCH 48/81] feat(dynamic-table): Hide action bar in responses table Hide Action bar from the dynamic table when viewing form responses from the group uploads component The action bar is accessible only from within the upload itself Refs Tangerine-Community/Tangerine#3703 --- .../group-uploads-view.component.html | 2 +- .../group-uploads/group-uploads.component.html | 2 +- .../app/groups/responses/responses.component.html | 2 +- .../app/groups/responses/responses.component.ts | 10 ++++++++++ .../dynamic-table/dynamic-table.component.html | 8 ++++---- .../dynamic-table/dynamic-table.component.ts | 14 +++++++++----- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html index 66eddf3ac8..e6168120c0 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html @@ -6,4 +6,4 @@ - \ No newline at end of file +Ï€ \ No newline at end of file diff --git a/editor/src/app/groups/group-uploads/group-uploads.component.html b/editor/src/app/groups/group-uploads/group-uploads.component.html index 7888162d83..d2e40549ad 100644 --- a/editor/src/app/groups/group-uploads/group-uploads.component.html +++ b/editor/src/app/groups/group-uploads/group-uploads.component.html @@ -1,4 +1,4 @@
- +
diff --git a/editor/src/app/groups/responses/responses.component.html b/editor/src/app/groups/responses/responses.component.html index 362e34900f..36922e8d2a 100644 --- a/editor/src/app/groups/responses/responses.component.html +++ b/editor/src/app/groups/responses/responses.component.html @@ -10,7 +10,7 @@
- +
< back next > diff --git a/editor/src/app/groups/responses/responses.component.ts b/editor/src/app/groups/responses/responses.component.ts index a3df81ea5b..e0a797e036 100644 --- a/editor/src/app/groups/responses/responses.component.ts +++ b/editor/src/app/groups/responses/responses.component.ts @@ -22,6 +22,7 @@ export class ResponsesComponent implements OnInit { @Input() excludeForms:Array = [] @Input() excludeColumns:Array = [] @Input() hideFilterBy = false + @Input() hideActionBar = false @ViewChild('searchBar', {static: true}) searchBar: ElementRef @ViewChild('searchResults', {static: true}) searchResults: ElementRef onSearch$ = new Subject() @@ -128,6 +129,12 @@ export class ResponsesComponent implements OnInit { this.skip = 0; this.getResponses(); } + async deleteResponse(id) { + if(confirm('Are you sure you want to delete this form response?')) { + await this.http.delete(`/api/${this.groupId}/${id}`).toPromise() + this.getResponses() + } + } nextPage() { this.skip = this.skip + this.limit if(this.searchString){ @@ -149,6 +156,9 @@ export class ResponsesComponent implements OnInit { onRowEdit(row) { this.router.navigate([row._id ? row._id : row.id], {relativeTo: this.route}) } + onRowDelete(row) { + this.deleteResponse(row._id ? row._id : row.id) + } onRowClick(row){ this.router.navigate([row._id ? row._id : row.id], {relativeTo: this.route}) } diff --git a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.html b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.html index 2ceba62697..eb27e083f6 100644 --- a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.html +++ b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.html @@ -5,10 +5,10 @@ {{ column.cell(row) }} - + -
+
@@ -26,6 +26,6 @@ - + - \ No newline at end of file + diff --git a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.ts b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.ts index 9be8200668..09879e03c8 100644 --- a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.ts +++ b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.ts @@ -13,6 +13,7 @@ export class DynamicTableComponent implements OnInit, OnChanges { @Output() rowDelete: EventEmitter = new EventEmitter(); @Input() data:Array = [] @Input() columnLabels = {} + @Input() hideActionBar columns:Array displayedColumns:Array dataSource:any @@ -44,19 +45,22 @@ export class DynamicTableComponent implements OnInit, OnChanges { cell: (element: any) => `${element[column] ? element[column] : ``}` } }) - this.displayedColumns = [...this.columns.map(c => c.columnDef), 'actions']; + this.displayedColumns = this.hideActionBar? [...this.columns.map(c => c.columnDef)]:[...this.columns.map(c => c.columnDef), 'actions'] // Set the dataSource for . this.dataSource = this.data } - onRowClick(row) { - this.rowClick.emit(row) - } + onRowClick(event,row) { + if (event.target.tagName === 'MAT-ICON'){ + event.stopPropagation() + }else{ + this.rowClick.emit(row) + } + } onRowEdit(row) { this.rowEdit.emit(row) } - onRowDelete(row) { this.rowDelete.emit(row) } From 8480a1356fcbadbf0849442a3cc09bde9beb8636 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 5 Jun 2024 23:05:29 +0300 Subject: [PATCH 49/81] feat(editor): change unlockFormResponses to be an attribute instead of seeting directly in code Refs Tangerine-Community/Tangerine#3703 --- .../groups/group-uploads-edit/group-uploads-edit.component.html | 2 +- .../groups/group-uploads-edit/group-uploads-edit.component.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.html b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.html index ba6d57d67a..b88daa91a5 100644 --- a/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.html +++ b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.html @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.ts b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.ts index a524475a15..953098eec1 100644 --- a/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.ts +++ b/editor/src/app/groups/group-uploads-edit/group-uploads-edit.component.ts @@ -37,7 +37,6 @@ export class GroupUploadsEditComponent implements OnInit { } ] this.formPlayer.formResponseId = params.responseId - this.formPlayer.unlockFormResponses = true this.formPlayer.render() this.formPlayer.$submit.subscribe(async () => { this.formPlayer.saveResponse(this.formPlayer.formEl.store.getState()) From a11878e6263e3a4b647742bf6022b3fd23be1cdc Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 5 Jun 2024 23:06:47 +0300 Subject: [PATCH 50/81] feat(editor): Add verification for uploads - Use the userId from the JWT as the one from the local storage can be manipulated Refs Tangerine-Community/Tangerine#3703 --- .../group-uploads-view.component.ts | 11 ++++++++++- .../group-responses/group-responses.controller.ts | 9 ++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts index 6d4f3914fd..014995aed5 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts @@ -4,6 +4,7 @@ import { Breadcrumb } from './../../shared/_components/breadcrumb/breadcrumb.com import { _TRANSLATE } from 'src/app/shared/_services/translation-marker'; import { Component, OnInit, ViewChild } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { TangyFormService } from 'src/app/tangy-forms/tangy-form.service'; @Component({ selector: 'app-group-uploads-view', @@ -22,6 +23,7 @@ export class GroupUploadsViewComponent implements OnInit { private route:ActivatedRoute, private router:Router, private http: HttpClient, + private tangyFormService: TangyFormService, ) { } ngOnInit() { @@ -55,7 +57,14 @@ export class GroupUploadsViewComponent implements OnInit { } async verify(){ - + try { + const data = {...this.formPlayer.formEl.store.getState(), verified:true}; + await this.tangyFormService.saveResponse(data) + alert(_TRANSLATE('Verified successfully.')) + } catch (error) { + alert(_TRANSLATE('Verification was unsuccessful. Please try again.')) + console.log(error) + } } } diff --git a/server/src/core/group-responses/group-responses.controller.ts b/server/src/core/group-responses/group-responses.controller.ts index 0a88ec64f1..c6e61034bf 100644 --- a/server/src/core/group-responses/group-responses.controller.ts +++ b/server/src/core/group-responses/group-responses.controller.ts @@ -1,6 +1,8 @@ import { GroupResponsesService } from './../../shared/services/group-responses/group-responses.service'; -import { Controller, All, Param, Body } from '@nestjs/common'; +import { Controller, All, Param, Body , Req} from '@nestjs/common'; import { SSL_OP_TLS_BLOCK_PADDING_BUG } from 'constants'; +import { Request } from 'express'; +import { decodeJWT } from 'src/auth-utils'; const log = require('tangy-log').log @Controller('group-responses') @@ -48,8 +50,9 @@ export class GroupResponsesController { } @All('update/:groupId') - async update(@Param('groupId') groupId, @Body('response') response:any) { - const freshResponse = await this.groupResponsesService.update(groupId, response) + async update(@Param('groupId') groupId, @Body('response') response:any, @Req() request:Request) { + const tangerineModifiedByUserId = decodeJWT(request['headers']['authorization'])['username'] + const freshResponse = await this.groupResponsesService.update(groupId, {...response, tangerineModifiedByUserId}) return freshResponse } From ebc58d8498b4f6121a128776b13c056dde9e3d62 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 5 Jun 2024 23:40:20 +0300 Subject: [PATCH 51/81] feat(responses-table): Add verified to list of columns The column verified should be on the responses table Refs Tangerine-Community/Tangerine#3703 --- editor/src/app/groups/responses/tangy-form-response-flatten.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/app/groups/responses/tangy-form-response-flatten.ts b/editor/src/app/groups/responses/tangy-form-response-flatten.ts index 6b5dc723aa..5c66689a01 100644 --- a/editor/src/app/groups/responses/tangy-form-response-flatten.ts +++ b/editor/src/app/groups/responses/tangy-form-response-flatten.ts @@ -22,6 +22,7 @@ export const generateFlatResponse = async function (formResponse, locationLists, deviceId: formResponse.deviceId||'', groupId: formResponse.groupId||'', complete: formResponse.complete, + verified: formResponse.verified||'', tangerineModifiedByUserId: formResponse.tangerineModifiedByUserId, ...formResponse.caseId ? { caseId: formResponse.caseId, From f378b3286291cf23e1da0271603d931fc815b21f Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 5 Jun 2024 23:51:31 +0300 Subject: [PATCH 52/81] feat(editor): Show message to user in case the verification was not successfully Refs Tangerine-Community/Tangerine#3703 --- .../group-uploads-view/group-uploads-view.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts index 014995aed5..b59f17911e 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts @@ -59,8 +59,12 @@ export class GroupUploadsViewComponent implements OnInit { async verify(){ try { const data = {...this.formPlayer.formEl.store.getState(), verified:true}; - await this.tangyFormService.saveResponse(data) - alert(_TRANSLATE('Verified successfully.')) + const result = await this.tangyFormService.saveResponse(data) + if(result){ + alert(_TRANSLATE('Verified successfully.')) + }else{ + alert(_TRANSLATE('Verification was unsuccessful. Please try again.')) + } } catch (error) { alert(_TRANSLATE('Verification was unsuccessful. Please try again.')) console.log(error) From 0c9973a64ca7485d0b13f6c9786d16e938f82418 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Fri, 7 Jun 2024 15:21:37 +0300 Subject: [PATCH 53/81] fix(import-user-profile): Limit the total docs to the specific user's documents Refs Tangerine-Community/Tangerine#3696 --- .../import-user-profile/import-user-profile.component.css | 2 +- .../src/routes/group-responses-by-user-profile-short-code.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.css b/client/src/app/user-profile/import-user-profile/import-user-profile.component.css index 14a4ec4f68..31720ef5d3 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.css +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.css @@ -8,4 +8,4 @@ paper-input { } #err { color: red; - } \ No newline at end of file + } diff --git a/server/src/routes/group-responses-by-user-profile-short-code.js b/server/src/routes/group-responses-by-user-profile-short-code.js index 9f397bef56..58a9139e9e 100644 --- a/server/src/routes/group-responses-by-user-profile-short-code.js +++ b/server/src/routes/group-responses-by-user-profile-short-code.js @@ -14,9 +14,7 @@ module.exports = async (req, res) => { options.skip = req.params.skip } if (req.query.totalRows) { - options.limit = 1 - options.skip = 0 - const results = await groupDb.query('responsesByUserProfileShortCode', options); + const results = await groupDb.query('responsesByUserProfileShortCode', { key: userProfileShortCode, limit: 1,skip: 0, include_docs: false }); res.send({ totalDocs: results.total_rows }) } else if (req.query.userProfile) { await groupDb.query("userProfileByUserProfileShortCode", { limit: 0 }); From e84d4e5d36b5ad8745001ef750b6812fca4d5ee7 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 12 Jun 2024 22:57:34 +0300 Subject: [PATCH 54/81] feat(group-uploads-verify): Return to uploads page after successfully verifying a response After the 'verify' button is clicked, navigate back to the uploads table because it's a 'completion' action. Refs Tangerine-Community/Tangerine#3703 --- .../groups/group-uploads-view/group-uploads-view.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts index b59f17911e..f283f0a836 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts @@ -62,6 +62,7 @@ export class GroupUploadsViewComponent implements OnInit { const result = await this.tangyFormService.saveResponse(data) if(result){ alert(_TRANSLATE('Verified successfully.')) + this.router.navigate([`../`], { relativeTo: this.route }) }else{ alert(_TRANSLATE('Verification was unsuccessful. Please try again.')) } From ce5497d71581b5dea305fba9be928e8f6ddeb86a Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 12 Jun 2024 23:06:25 +0300 Subject: [PATCH 55/81] fix(buttons): Change color of buttons to be the Tangerine accent color Refs Tangerine-Community/Tangerine#3703 --- .../group-uploads-view/group-uploads-view.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html index e6168120c0..3f29f9c4cc 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html @@ -1,9 +1,9 @@
- + - {{'Edit'|translate}} + {{'Edit'|translate}} - +
-Ï€ \ No newline at end of file + From 3075f81135d6cf57bdd56a6f67f254c9334f15df Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 13 Jun 2024 09:55:12 +0300 Subject: [PATCH 56/81] fix(csv): Add missing columns for Attendace and reporting Add `reportDate` property to the `flattenedFormResponse` Refs Tangerine-Community/Tangerine#3707 --- server/src/modules/csv/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/modules/csv/index.js b/server/src/modules/csv/index.js index 0e47189a7a..bd8e510dec 100644 --- a/server/src/modules/csv/index.js +++ b/server/src/modules/csv/index.js @@ -260,6 +260,7 @@ const generateFlatResponse = async function (formResponse, sanitized, groupId) flatFormResponse['grade'] = formResponse.grade flatFormResponse['schoolName'] = formResponse.schoolName flatFormResponse['schoolYear'] = formResponse.schoolYear + flatFormResponse['reportDate'] = formResponse.reportDate flatFormResponse['type'] = formResponse.type if (formResponse.type === 'attendance') { flatFormResponse['attendanceList'] = formResponse.attendanceList From a2f1b7f97b76cbf7d3a36880e2c3b5b0a2996c8b Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 13 Jun 2024 12:36:09 +0300 Subject: [PATCH 57/81] feat(editor): Allow users to Add custom code Javascript in Feedback page Refs Tangerine-Community/Tangerine#3706 --- .../feedback-editor/feedback-editor.component.html | 3 +++ .../feedback-editor/feedback-editor.component.ts | 4 ++++ .../src/app/ng-tangy-form-editor/feedback-editor/feedback.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback-editor.component.html b/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback-editor.component.html index ba4c160eae..bfa9f1dae1 100644 --- a/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback-editor.component.html +++ b/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback-editor.component.html @@ -89,6 +89,9 @@

Tasks/Subtasks with Feedback

+ + +
diff --git a/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback-editor.component.ts b/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback-editor.component.ts index 90930f35c9..78fd4c939b 100644 --- a/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback-editor.component.ts +++ b/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback-editor.component.ts @@ -25,6 +25,7 @@ export class FeedbackEditorComponent implements OnInit { skill:string = "" assignment:string = "" message:string = "" + customJSCode:string = "" formItems:any formItem:string formItemName:string @@ -122,6 +123,7 @@ export class FeedbackEditorComponent implements OnInit { this.skill = "" this.assignment = "" this.message = "" + this.customJSCode = "" this.formItem = "" this.showFeedbackForm = true @@ -140,6 +142,7 @@ export class FeedbackEditorComponent implements OnInit { this.feedback.skill = this.skill this.feedback.assignment = this.assignment this.feedback.message = this.message + this.feedback.customJSCode = this.customJSCode this.feedback.formItem = this.formItem this.feedbackService.update(this.groupName, this.formId, this.feedback) .then(async (data) => { @@ -168,6 +171,7 @@ export class FeedbackEditorComponent implements OnInit { this.skill = this.feedback.skill this.assignment = this.feedback.assignment this.message = this.feedback.message + this.customJSCode = this.feedback.customJSCode this.formItem = this.feedback.formItem this.percentileOptions = this.createPercentileOptions(null) diff --git a/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback.ts b/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback.ts index 3523989abe..97adf98a6b 100644 --- a/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback.ts +++ b/editor/src/app/ng-tangy-form-editor/feedback-editor/feedback.ts @@ -8,5 +8,6 @@ export class Feedback { skill:string; assignment:string; message:string; + customJSCode: string; messageTruncated: string; // for listing } From 565fcc413e1d97dc485697d9ab3b4e88da141b19 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Fri, 21 Jun 2024 14:43:38 +0300 Subject: [PATCH 58/81] feat(client): Add Custom JS code Run Custom JS Code on Dialog Init Refs Tangerine-Community/Tangerine#3706 --- client/src/app/class/feedback.ts | 1 + .../student-grouping-report.component.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/app/class/feedback.ts b/client/src/app/class/feedback.ts index a915e5f1d4..40e2e8d4b8 100644 --- a/client/src/app/class/feedback.ts +++ b/client/src/app/class/feedback.ts @@ -7,6 +7,7 @@ export class Feedback { skill:string; assignment:string; message:string; + customJSCode: string; messageTruncated: string; // for listing calculatedScore:string; percentileRange: string; diff --git a/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.ts b/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.ts index e1372c4688..eafce5ea4c 100644 --- a/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.ts +++ b/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.ts @@ -215,13 +215,17 @@ export interface DialogData { selector: 'feedback-dialog', templateUrl: 'feedback-dialog.html', }) -export class FeedbackDialog { +export class FeedbackDialog implements OnInit { constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: DialogData, ) { } - + async ngOnInit(){ + if(this.data.classGroupReport?.feedback?.customJSCode){ + eval(this.data.classGroupReport?.feedback?.customJSCode) + } + } tNumber(fragment) { return tNumber(fragment) } From e0451f88b441325c824b9d7c9ca30791ada4d237 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 26 Jun 2024 14:07:05 +0300 Subject: [PATCH 59/81] feat(verification): Allow for Unarchiving and Unverifying * Allow for archiving from the form response * Allow for unverification and unarchival Refs Tangerine-Community/Tangerine#3703 --- .../group-uploads-view.component.html | 10 ++-- .../group-uploads-view.component.ts | 57 +++++++++++++++++-- .../group-uploads.component.html | 2 +- .../groups/responses/responses.component.html | 2 +- .../groups/responses/responses.component.ts | 24 ++++++-- .../responses/tangy-form-response-flatten.ts | 2 + .../dynamic-table.component.html | 9 ++- .../dynamic-table/dynamic-table.component.ts | 5 ++ .../group-responses.controller.ts | 9 ++- server/src/modules/csv/index.js | 6 +- server/src/modules/logstash/index.js | 6 +- server/src/modules/mysql-js/index.js | 5 +- server/src/modules/mysql/index.js | 5 +- server/src/modules/rshiny/index.js | 6 +- server/src/modules/synapse/index.js | 6 +- .../group-responses.service.ts | 4 +- 16 files changed, 127 insertions(+), 31 deletions(-) diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html index 3f29f9c4cc..4d961bd6b5 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.html @@ -1,9 +1,11 @@
- - + + + {{'Edit'|translate}} - - + + +
diff --git a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts index f283f0a836..d0ef8fe862 100644 --- a/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts +++ b/editor/src/app/groups/group-uploads-view/group-uploads-view.component.ts @@ -18,6 +18,7 @@ export class GroupUploadsViewComponent implements OnInit { @ViewChild('formPlayer', {static: true}) formPlayer: TangyFormsPlayerComponent responseId groupId + formResponse constructor( private route:ActivatedRoute, @@ -26,8 +27,8 @@ export class GroupUploadsViewComponent implements OnInit { private tangyFormService: TangyFormService, ) { } - ngOnInit() { - this.route.params.subscribe(params => { + async ngOnInit() { + this.route.params.subscribe(async params => { this.responseId = params.responseId this.groupId = params.groupId this.breadcrumbs = [ @@ -41,6 +42,7 @@ export class GroupUploadsViewComponent implements OnInit { } ] this.formPlayer.formResponseId = params.responseId + this.formResponse = await this.tangyFormService.getResponse(params.responseId) this.formPlayer.render() this.formPlayer.$submit.subscribe(async () => { this.formPlayer.saveResponse(this.formPlayer.formEl.store.getState()) @@ -49,10 +51,38 @@ export class GroupUploadsViewComponent implements OnInit { }) } - async delete(){ - if(confirm('Are you sure you want to delete this form response?')) { - await this.http.delete(`/api/${this.groupId}/${this.responseId}`).toPromise() - this.router.navigate([`../`], { relativeTo: this.route }) + async archive(){ + if(confirm('Are you sure you want to archive this form response?')) { + try { + const data = {...this.formPlayer.formEl.store.getState(), archived:true}; + const result = await this.tangyFormService.saveResponse(data) + if(result){ + alert(_TRANSLATE('Archived successfully.')) + this.router.navigate([`../`], { relativeTo: this.route }) + }else{ + alert(_TRANSLATE('Archival was unsuccessful. Please try again.')) + } + } catch (error) { + alert(_TRANSLATE('Archival was unsuccessful. Please try again.')) + console.log(error) + } + } + } + async unarchive(){ + if(confirm(_TRANSLATE('Are you sure you want to unarchive this form response?'))) { + try { + const data = {...this.formPlayer.formEl.store.getState(), archived:false}; + const result = await this.tangyFormService.saveResponse(data) + if(result){ + alert(_TRANSLATE('Unarchived successfully.')) + this.router.navigate([`../`], { relativeTo: this.route }) + }else{ + alert(_TRANSLATE('Unarchival was unsuccessful. Please try again.')) + } + } catch (error) { + alert(_TRANSLATE('Unarchival was unsuccessful. Please try again.')) + console.log(error) + } } } @@ -71,5 +101,20 @@ export class GroupUploadsViewComponent implements OnInit { console.log(error) } } + async unverify(){ + try { + const data = {...this.formPlayer.formEl.store.getState(), verified:false}; + const result = await this.tangyFormService.saveResponse(data) + if(result){ + alert(_TRANSLATE('Unverified successfully.')) + this.router.navigate([`../`], { relativeTo: this.route }) + }else{ + alert(_TRANSLATE('Unverification was unsuccessful. Please try again.')) + } + } catch (error) { + alert(_TRANSLATE('Unverification was unsuccessful. Please try again.')) + console.log(error) + } + } } diff --git a/editor/src/app/groups/group-uploads/group-uploads.component.html b/editor/src/app/groups/group-uploads/group-uploads.component.html index d2e40549ad..4e6cd45b92 100644 --- a/editor/src/app/groups/group-uploads/group-uploads.component.html +++ b/editor/src/app/groups/group-uploads/group-uploads.component.html @@ -1,4 +1,4 @@
- +
diff --git a/editor/src/app/groups/responses/responses.component.html b/editor/src/app/groups/responses/responses.component.html index 36922e8d2a..dc2f796b0a 100644 --- a/editor/src/app/groups/responses/responses.component.html +++ b/editor/src/app/groups/responses/responses.component.html @@ -10,7 +10,7 @@
- +
< back next > diff --git a/editor/src/app/groups/responses/responses.component.ts b/editor/src/app/groups/responses/responses.component.ts index e0a797e036..3529df8b17 100644 --- a/editor/src/app/groups/responses/responses.component.ts +++ b/editor/src/app/groups/responses/responses.component.ts @@ -9,6 +9,7 @@ import * as moment from 'moment' import { t } from 'tangy-form/util/t.js' import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; +import { _TRANSLATE } from 'src/app/shared/translation-marker'; @Component({ selector: 'app-responses', @@ -23,6 +24,7 @@ export class ResponsesComponent implements OnInit { @Input() excludeColumns:Array = [] @Input() hideFilterBy = false @Input() hideActionBar = false + @Input() showArchiveButton = false @ViewChild('searchBar', {static: true}) searchBar: ElementRef @ViewChild('searchResults', {static: true}) searchResults: ElementRef onSearch$ = new Subject() @@ -129,11 +131,21 @@ export class ResponsesComponent implements OnInit { this.skip = 0; this.getResponses(); } - async deleteResponse(id) { - if(confirm('Are you sure you want to delete this form response?')) { - await this.http.delete(`/api/${this.groupId}/${id}`).toPromise() - this.getResponses() + async archiveResponse(id) { + try { + if(confirm(_TRANSLATE('Are you sure you want to archive this form response?'))) { + const result = await this.http.patch(`/group-responses/patch/${this.groupId}/${id}`,{archived:true}).toPromise() + if(result){ + alert(_TRANSLATE('Archived successfully.')) + this.getResponses() + }else{ + alert(_TRANSLATE('Archival was unsuccessful. Please try again.')) + } } + } catch (error) { + alert(_TRANSLATE('Archival was unsuccessful. Please try again.')) + console.log(error) + } } nextPage() { this.skip = this.skip + this.limit @@ -156,8 +168,8 @@ export class ResponsesComponent implements OnInit { onRowEdit(row) { this.router.navigate([row._id ? row._id : row.id], {relativeTo: this.route}) } - onRowDelete(row) { - this.deleteResponse(row._id ? row._id : row.id) + onRowArchive(row) { + this.archiveResponse(row._id ? row._id : row.id) } onRowClick(row){ this.router.navigate([row._id ? row._id : row.id], {relativeTo: this.route}) diff --git a/editor/src/app/groups/responses/tangy-form-response-flatten.ts b/editor/src/app/groups/responses/tangy-form-response-flatten.ts index 5c66689a01..411dd9314e 100644 --- a/editor/src/app/groups/responses/tangy-form-response-flatten.ts +++ b/editor/src/app/groups/responses/tangy-form-response-flatten.ts @@ -23,6 +23,8 @@ export const generateFlatResponse = async function (formResponse, locationLists, groupId: formResponse.groupId||'', complete: formResponse.complete, verified: formResponse.verified||'', + archived: formResponse.archived||'', + tangerineModifiedOn: formResponse.tangerineModifiedOn||'', tangerineModifiedByUserId: formResponse.tangerineModifiedByUserId, ...formResponse.caseId ? { caseId: formResponse.caseId, diff --git a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.html b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.html index eb27e083f6..0b632daf5c 100644 --- a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.html +++ b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.html @@ -14,10 +14,13 @@ - +
diff --git a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.ts b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.ts index 09879e03c8..63d8f68c67 100644 --- a/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.ts +++ b/editor/src/app/shared/_components/dynamic-table/dynamic-table.component.ts @@ -11,9 +11,11 @@ export class DynamicTableComponent implements OnInit, OnChanges { @Output() rowClick: EventEmitter = new EventEmitter(); @Output() rowEdit: EventEmitter = new EventEmitter(); @Output() rowDelete: EventEmitter = new EventEmitter(); + @Output() rowArchive: EventEmitter = new EventEmitter(); @Input() data:Array = [] @Input() columnLabels = {} @Input() hideActionBar + @Input() showArchiveButton columns:Array displayedColumns:Array dataSource:any @@ -64,6 +66,9 @@ export class DynamicTableComponent implements OnInit, OnChanges { onRowDelete(row) { this.rowDelete.emit(row) } + onRowArchive(row) { + this.rowArchive.emit(row) + } diff --git a/server/src/core/group-responses/group-responses.controller.ts b/server/src/core/group-responses/group-responses.controller.ts index c6e61034bf..1e7d683bb4 100644 --- a/server/src/core/group-responses/group-responses.controller.ts +++ b/server/src/core/group-responses/group-responses.controller.ts @@ -21,7 +21,6 @@ export class GroupResponsesController { async query(@Param('groupId') groupId, @Body('query') query) { return await this.groupResponsesService.find(groupId, query) } - @All('search/:groupId') async search(@Param('groupId') groupId, @Body('phrase') phrase, @Body('index') index) { return await this.groupResponsesService.search(groupId, phrase, index) @@ -61,5 +60,11 @@ export class GroupResponsesController { await this.groupResponsesService.delete(groupId, responseId) return {} } - + @All('patch/:groupId/:responseId') + async patch(@Param('groupId') groupId:string, @Param('responseId') responseId:string, @Req() request:Request) { + const tangerineModifiedByUserId = decodeJWT(request['headers']['authorization'])['username'] + const doc = await this.groupResponsesService.read(groupId, responseId) + const freshResponse = await this.groupResponsesService.update(groupId, {...doc,...request['body'],tangerineModifiedByUserId}) + return request['body'] + } } diff --git a/server/src/modules/csv/index.js b/server/src/modules/csv/index.js index 0e47189a7a..6f8957056f 100644 --- a/server/src/modules/csv/index.js +++ b/server/src/modules/csv/index.js @@ -243,9 +243,11 @@ const generateFlatResponse = async function (formResponse, sanitized, groupId) deviceId: formResponse.deviceId || '', groupId: formResponse.groupId || '', complete: formResponse.complete, + verified: formResponse?.verified||'', + tangerineModifiedOn: formResponse?.tangerineModifiedOn||'', // NOTE: Doubtful that anything with an archived flag would show up here because it would have been deleted already in 'Delete from the -reporting db.' - archived: formResponse.archived, - tangerineModifiedByUserId: formResponse.tangerineModifiedByUserId, + archived: formResponse?.archived||'', + tangerineModifiedByUserId: formResponse?.tangerineModifiedByUserId||'', ...formResponse.caseId ? { caseId: formResponse.caseId, eventId: formResponse.eventId, diff --git a/server/src/modules/logstash/index.js b/server/src/modules/logstash/index.js index 3f6a3d7fc5..a516f9cc96 100644 --- a/server/src/modules/logstash/index.js +++ b/server/src/modules/logstash/index.js @@ -115,7 +115,11 @@ const generateFlatResponse = async function (formResponse, locationList) { buildChannel: formResponse.buildChannel||'', deviceId: formResponse.deviceId||'', groupId: formResponse.groupId||'', - complete: formResponse.complete + complete: formResponse.complete, + verified: formResponse?.verified||'', + tangerineModifiedOn: formResponse?.tangerineModifiedOn||'', + tangerineModifiedByUserId: formResponse.tangerineModifiedByUserId||'', + archived: formResponse?.archived||'', }; let formID = formResponse.form.id; diff --git a/server/src/modules/mysql-js/index.js b/server/src/modules/mysql-js/index.js index 06f7e5c027..d1f7756d1e 100644 --- a/server/src/modules/mysql-js/index.js +++ b/server/src/modules/mysql-js/index.js @@ -319,7 +319,10 @@ const generateFlatResponse = async function (formResponse, sanitized) { deviceId: formResponse.deviceId||'', groupId: groupId||'', complete: formResponse.complete, - archived: formResponse.archived||'' + archived: formResponse?.archived||'', + tangerineModifiedOn: formResponse?.tangerineModifiedOn||'', + tangerineModifiedByUserId: formResponse?.tangerineModifiedByUserId||'', + verified: formResponse?.verified||'' }; if (!formResponse.formId) { diff --git a/server/src/modules/mysql/index.js b/server/src/modules/mysql/index.js index a630337c59..a7ef66fba5 100644 --- a/server/src/modules/mysql/index.js +++ b/server/src/modules/mysql/index.js @@ -266,7 +266,10 @@ const generateFlatResponse = async function (formResponse, sanitized) { deviceId: formResponse.deviceId||'', groupId: formResponse.groupId||'', complete: formResponse.complete, - archived: formResponse.archived||'' + archived: formResponse?.archived||'', + tangerineModifiedOn: formResponse?.tangerineModifiedOn||'', + tangerineModifiedByUserId: formResponse.tangerineModifiedByUserId|'', + verified: formResponse?.verified|'', }; for (let item of formResponse.items) { for (let input of item.inputs) { diff --git a/server/src/modules/rshiny/index.js b/server/src/modules/rshiny/index.js index c31ffa8722..53c8a3bfca 100644 --- a/server/src/modules/rshiny/index.js +++ b/server/src/modules/rshiny/index.js @@ -161,7 +161,11 @@ const generateFlatResponse = async function (formResponse, locationList, sanitiz buildChannel: formResponse.buildChannel||'', deviceId: formResponse.deviceId||'', groupId: formResponse.groupId||'', - complete: formResponse.complete + complete: formResponse.complete, + archived: formResponse?.archived||'', + tangerineModifiedOn: formResponse?.tangerineModifiedOn||'', + tangerineModifiedByUserId: formResponse?.tangerineModifiedByUserId||'', + verified: formResponse?.verified||'' }; function set(input, key, value) { flatFormResponse[key] = input.skipped diff --git a/server/src/modules/synapse/index.js b/server/src/modules/synapse/index.js index 7b48a5f3ba..c3bb54ec76 100644 --- a/server/src/modules/synapse/index.js +++ b/server/src/modules/synapse/index.js @@ -186,7 +186,11 @@ const generateFlatResponse = async function (formResponse, locationList, sanitiz buildChannel: formResponse.buildChannel||'', deviceId: formResponse.deviceId||'', groupId: formResponse.groupId||'', - complete: formResponse.complete + complete: formResponse.complete, + archived: formResponse?.archived||'', + tangerineModifiedOn: formResponse?.tangerineModifiedOn||'', + tangerineModifiedByUserId: formResponse?.tangerineModifiedByUserId||'', + verified: formResponse?.verified||'', }; function set(input, key, value) { flatFormResponse[key] = input.skipped diff --git a/server/src/shared/services/group-responses/group-responses.service.ts b/server/src/shared/services/group-responses/group-responses.service.ts index 01531fd362..f7bbdf1cdf 100644 --- a/server/src/shared/services/group-responses/group-responses.service.ts +++ b/server/src/shared/services/group-responses/group-responses.service.ts @@ -137,11 +137,13 @@ export class GroupResponsesService { } async update(groupId, response) { + const tangerineModifiedOn = Date.now() try { const groupDb = this.getGroupsDb(groupId) const originalResponse = await groupDb.get(response._id) await groupDb.put({ ...response, + tangerineModifiedOn, _rev: originalResponse._rev }) const freshResponse = await groupDb.get(response._id) @@ -149,7 +151,7 @@ export class GroupResponsesService { } catch (e) { try { const groupDb = this.getGroupsDb(groupId) - await groupDb.put(response) + await groupDb.put({...response, tangerineModifiedOn}) const freshResponse = await groupDb.get(response._id) return freshResponse } catch (e) { From 20d83621cb5dbce76d33915f3b6d9ee67d3a1055 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Mon, 8 Jul 2024 17:42:25 +0300 Subject: [PATCH 60/81] feat(editor): Disable GPS Refs Tangerine-Community/Tangerine#3703 --- .../tangy-forms-player/tangy-forms-player.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts index 95e6b0e321..ff52dc5131 100755 --- a/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts +++ b/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts @@ -194,7 +194,7 @@ export class TangyFormsPlayerComponent { this.$afterResubmit.next(true) }) } - this.unlockFormResponses? this.formEl.unlock(): null + this.unlockFormResponses? this.formEl.unlock({disableComponents:['TANGY-GPS']}): null this.$rendered.next(true) this.rendered = true } From 2812963182c528d7b7e7d7e7503d3139c597f99b Mon Sep 17 00:00:00 2001 From: esurface Date: Wed, 10 Jul 2024 11:47:39 -0400 Subject: [PATCH 61/81] Get all scores in attendance dashboard --- .../attendance-dashboard/attendance-dashboard.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/class/attendance/attendance-dashboard/attendance-dashboard.component.ts b/client/src/app/class/attendance/attendance-dashboard/attendance-dashboard.component.ts index 385d3a2c68..be5e03cceb 100644 --- a/client/src/app/class/attendance/attendance-dashboard/attendance-dashboard.component.ts +++ b/client/src/app/class/attendance/attendance-dashboard/attendance-dashboard.component.ts @@ -112,7 +112,7 @@ export class AttendanceDashboardComponent implements OnInit { for (let i = 0; i < this.currArray.length; i++) { const curriculum = this.currArray[i]; let curriculumLabel = curriculum?.label - const reports = await this.dashboardService.searchDocs('scores', currentClass, null, null, curriculumLabel, randomId, true) + const reports = await this.dashboardService.searchDocs('scores', currentClass, '*', null, curriculumLabel, randomId, true) reports.forEach((report) => { report.doc.curriculum = curriculum scoreReports.push(report.doc) From 1491dd3a33c89cefb29ab318a1a3228e4605bbf6 Mon Sep 17 00:00:00 2001 From: esurface Date: Wed, 10 Jul 2024 12:30:13 -0400 Subject: [PATCH 62/81] Fix bug setting the date for most recent report --- .../attendance/attendance.component.ts | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/client/src/app/class/reports/attendance/attendance.component.ts b/client/src/app/class/reports/attendance/attendance.component.ts index f2eb5dd0f4..e66c0343ae 100644 --- a/client/src/app/class/reports/attendance/attendance.component.ts +++ b/client/src/app/class/reports/attendance/attendance.component.ts @@ -117,6 +117,8 @@ export class AttendanceComponent implements OnInit { unitDate.endDate = endDate }) + this.reportLocaltime = DateTime.now().toLocaleString(DateTime.DATE_FULL) + // maybe make this relative to the selected date for the single day report table this.rangeStartDate = DateTime.now().minus({months: 1}).toJSDate(); this.rangeEndDate = DateTime.now().toJSDate(); @@ -205,17 +207,7 @@ export class AttendanceComponent implements OnInit { scoreReports.push(report.doc) }) } - const currentScoreReport = scoreReports[scoreReports.length - 1] - - if (currentAttendanceReport?.timestamp) { - const timestampFormatted = DateTime.fromMillis(currentAttendanceReport?.timestamp) - // DATE_MED - this.reportLocaltime = timestampFormatted.toLocaleString(DateTime.DATE_FULL) - } else { - this.reportLocaltime = DateTime.now().toLocaleString(DateTime.DATE_FULL) - } - - let scoreReport = currentScoreReport + let scoreReport = scoreReports[scoreReports.length - 1] if (startDate && endDate) { const selectedAttendanceReports = attendanceReports @@ -332,11 +324,6 @@ export class AttendanceComponent implements OnInit { selectStudentDetails(student) { student.ignoreCurriculumsForTracking = this.ignoreCurriculumsForTracking - const studentId = student.id; - const classId = student.classId; - // this.router.navigate(['student-details'], { queryParams: - // { studentId: studentId, classId: classId } - // }); this._bottomSheet.open(StudentDetailsComponent, { data: { student: student, @@ -352,12 +339,26 @@ export class AttendanceComponent implements OnInit { this.setBackButton(updatedIndex) this.currentIndex = updatedIndex this.attendanceReport = await this.generateSummaryReport(this.currArray, this.curriculum, this.selectedClass, this.classId, this.currentIndex, null, null); + + if ( this.attendanceReport?.timestamp) { + const timestampFormatted = DateTime.fromMillis( this.attendanceReport?.timestamp) + this.reportLocaltime = timestampFormatted.toLocaleString(DateTime.DATE_FULL) + } else { + this.reportLocaltime = DateTime.now().toLocaleString(DateTime.DATE_FULL) + } } async goForward() { const updatedIndex = this.currentIndex - 1 this.setForwardButton(updatedIndex); this.currentIndex = updatedIndex this.attendanceReport = await this.generateSummaryReport(this.currArray, this.curriculum, this.selectedClass, this.classId, this.currentIndex, null, null); + + if ( this.attendanceReport?.timestamp) { + const timestampFormatted = DateTime.fromMillis( this.attendanceReport?.timestamp) + this.reportLocaltime = timestampFormatted.toLocaleString(DateTime.DATE_FULL) + } else { + this.reportLocaltime = DateTime.now().toLocaleString(DateTime.DATE_FULL) + } } private setBackButton(updatedIndex) { From 05d7c9327e5a91471ef45a42eb7ab8e31a2743d1 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 11 Jul 2024 13:25:08 +0300 Subject: [PATCH 63/81] feat(import-docs): Add view for calculating user docs Add views for partitioning user docs by userProfileShortCode Refs Tangerine-Community/Tangerine#3696 --- server/src/group-views.js | 18 ++++++++++++++++++ ...oup-responses-by-user-profile-short-code.js | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/server/src/group-views.js b/server/src/group-views.js index 1d8e7725f3..db658c92b7 100644 --- a/server/src/group-views.js +++ b/server/src/group-views.js @@ -82,6 +82,24 @@ module.exports.userProfileByUserProfileShortCode = function (doc) { return emit(doc._id.substr(doc._id.length - 6, doc._id.length), true); } } +module.exports.totalDocsByUserProfileShortCode = { + map: function (doc) { + if (doc.form && doc.form.id === 'user-profile') { + return emit(doc._id.substr(doc._id.length-6, doc._id.length), 1) + } + var inputs = doc.items.reduce(function(acc, item) { return acc.concat(item.inputs)}, []) + var userProfileInput = null + inputs.forEach(function(input) { + if (input.name === 'userProfileId') { + userProfileInput = input + } + }) + if (userProfileInput) { + emit(userProfileInput.value.substr(userProfileInput.value.length-6, userProfileInput.value.length), 1) + } + }, + reduce: '_count' +} module.exports.groupIssues = function(doc) { if (doc.collection === "TangyFormResponse" && doc.type === "issue") { diff --git a/server/src/routes/group-responses-by-user-profile-short-code.js b/server/src/routes/group-responses-by-user-profile-short-code.js index 58a9139e9e..b16c1f26b6 100644 --- a/server/src/routes/group-responses-by-user-profile-short-code.js +++ b/server/src/routes/group-responses-by-user-profile-short-code.js @@ -14,8 +14,9 @@ module.exports = async (req, res) => { options.skip = req.params.skip } if (req.query.totalRows) { - const results = await groupDb.query('responsesByUserProfileShortCode', { key: userProfileShortCode, limit: 1,skip: 0, include_docs: false }); - res.send({ totalDocs: results.total_rows }) + const results = await groupDb.query('totalDocsByUserProfileShortCode', { key: userProfileShortCode, limit: 1,skip: 0, include_docs: false, reduce:true, group:true }); + console.log(results) + res.send({ totalDocs: results.rows[0].value }) } else if (req.query.userProfile) { await groupDb.query("userProfileByUserProfileShortCode", { limit: 0 }); const result = await groupDb.query("userProfileByUserProfileShortCode", { key: userProfileShortCode, limit: 1, include_docs: true }); From 1934ea94a555a3b79d087d9c51c51583ede4cb47 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Thu, 11 Jul 2024 18:27:35 +0300 Subject: [PATCH 64/81] feat(student-grouping): Expose Student ID property Refs Tangerine-Community/Tangerine#3706 --- .../student-grouping-report.component.html | 8 ++++---- .../student-grouping-report.component.ts | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.html b/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.html index c54f03e267..9d9131a420 100644 --- a/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.html +++ b/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.html @@ -59,13 +59,13 @@

{{'Student'|translate}} - {{element["name"]}} + {{element["name"]}} {{'Score'|translate}} - + {{element.customScore?element.customScore:tNumber(element.score)}} / @@ -77,7 +77,7 @@

{{'Percentile'|translate}} - + {{tNumber(element.scorePercentageCorrect)}} % @@ -85,7 +85,7 @@

{{'Status'|translate}} - + {{element.status}} diff --git a/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.ts b/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.ts index eafce5ea4c..88cc172540 100644 --- a/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.ts +++ b/client/src/app/class/reports/student-grouping-report/student-grouping-report.component.ts @@ -154,7 +154,7 @@ export class StudentGroupingReportComponent implements OnInit { } } - async getFeedbackForPercentile(percentile, curriculumId, itemId, name) { + async getFeedbackForPercentile(percentile, curriculumId, itemId, name, studentId) { // console.log("Get feedback for " + JSON.stringify(element)) const feedback: Feedback = await this.dashboardService.getFeedback(percentile, curriculumId, itemId); if (feedback) { @@ -163,7 +163,7 @@ export class StudentGroupingReportComponent implements OnInit { } // this.checkFeedbackMessagePosition = true; const dialogRef = this.dialog.open(FeedbackDialog, { - data: {classGroupReport: this.classGroupReport, name: name}, + data: {classGroupReport: this.classGroupReport, name: name, studentId}, width: '95vw', maxWidth: '95vw', }); @@ -209,6 +209,7 @@ export class StudentGroupingReportComponent implements OnInit { export interface DialogData { classGroupReport: ClassGroupingReport; name: string; + studentId } @Component({ From 95f0df6c6f3a7ef0e8e05423456299cdeee2b1ac Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 11 Jul 2024 11:40:27 -0400 Subject: [PATCH 65/81] Remove console logs --- server/src/routes/group-responses-by-user-profile-short-code.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/routes/group-responses-by-user-profile-short-code.js b/server/src/routes/group-responses-by-user-profile-short-code.js index b16c1f26b6..80e6041e6e 100644 --- a/server/src/routes/group-responses-by-user-profile-short-code.js +++ b/server/src/routes/group-responses-by-user-profile-short-code.js @@ -15,7 +15,6 @@ module.exports = async (req, res) => { } if (req.query.totalRows) { const results = await groupDb.query('totalDocsByUserProfileShortCode', { key: userProfileShortCode, limit: 1,skip: 0, include_docs: false, reduce:true, group:true }); - console.log(results) res.send({ totalDocs: results.rows[0].value }) } else if (req.query.userProfile) { await groupDb.query("userProfileByUserProfileShortCode", { limit: 0 }); @@ -29,7 +28,6 @@ module.exports = async (req, res) => { res.send(docs) } } catch (error) { - console.log(error) log.error(error); res.status(500).send(error); } From 3c6f514a28eb9b25d7f7a6ebb88aeaf4e09a56eb Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Mon, 15 Jul 2024 14:42:13 +0300 Subject: [PATCH 66/81] fix(client): trailing slash makes client not reach endpoint Refs Tangerine-Community/Tangerine#3706 --- .../import-user-profile/import-user-profile.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index 971a5aeba2..89b524ddaf 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -56,15 +56,15 @@ export class ImportUserProfileComponent implements AfterContentInit { try { this.appConfig = await this.appConfigService.getAppConfig() this.shortCode = this.userShortCodeInput.nativeElement.value; - let newUserProfile = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?userProfile=true`).toPromise() + let newUserProfile = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?userProfile=true`).toPromise() if(!!newUserProfile){ this.state = this.STATE_SYNCING await this.userService.saveUserAccount({ ...this.userAccount, userUUID: newUserProfile['_id'], initialProfileComplete: true }) - this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?totalRows=true`).toPromise())['totalDocs'] + this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?totalRows=true`).toPromise())['totalDocs'] const docsToQuery = 1000; let processedDocs = +localStorage.getItem('processedDocs') || 0; while (processedDocs < this.totalDocs) { - this.docs = await this.http.get(`${this.appConfig.serverUrl}/api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/${docsToQuery}/${processedDocs}`).toPromise() + this.docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/${docsToQuery}/${processedDocs}`).toPromise() for (let doc of this.docs) { delete doc._rev await this.db.put(doc) From a405449a9c60523a58b09ef2c3114386b1ed0183 Mon Sep 17 00:00:00 2001 From: Evans Dianga Date: Wed, 17 Jul 2024 13:11:38 +0300 Subject: [PATCH 67/81] feat(import-user-profiles): namespace `processedDocs` Refs Tangerine-Community/Tangerine#3696 --- .../import-user-profile/import-user-profile.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index 89b524ddaf..e3ec22666a 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -58,11 +58,12 @@ export class ImportUserProfileComponent implements AfterContentInit { this.shortCode = this.userShortCodeInput.nativeElement.value; let newUserProfile = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?userProfile=true`).toPromise() if(!!newUserProfile){ + const username = this.userService.getCurrentUser() this.state = this.STATE_SYNCING await this.userService.saveUserAccount({ ...this.userAccount, userUUID: newUserProfile['_id'], initialProfileComplete: true }) this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?totalRows=true`).toPromise())['totalDocs'] const docsToQuery = 1000; - let processedDocs = +localStorage.getItem('processedDocs') || 0; + let processedDocs = +localStorage.getItem(`${username}-processedDocs`) || 0; while (processedDocs < this.totalDocs) { this.docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/${docsToQuery}/${processedDocs}`).toPromise() for (let doc of this.docs) { @@ -71,7 +72,7 @@ export class ImportUserProfileComponent implements AfterContentInit { } processedDocs += this.docs.length; this.processedDocs = processedDocs - localStorage.setItem('processedDocs', String(processedDocs)) + localStorage.setItem(`${username}-processedDocs`, String(processedDocs)) } } else{ this.state = this.STATE_NOT_FOUND From a72c09c4b77878a42bfe644d0a95f885432a78c1 Mon Sep 17 00:00:00 2001 From: esurface Date: Wed, 17 Jul 2024 12:28:05 -0400 Subject: [PATCH 68/81] Ignore error if login custom markup does not exist --- .../src/app/core/auth/_services/authentication.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/editor/src/app/core/auth/_services/authentication.service.ts b/editor/src/app/core/auth/_services/authentication.service.ts index 2469d97612..1431f2b0b8 100644 --- a/editor/src/app/core/auth/_services/authentication.service.ts +++ b/editor/src/app/core/auth/_services/authentication.service.ts @@ -213,6 +213,10 @@ export class AuthenticationService { } async getCustomLoginMarkup() { - return await this.http.get('/custom-login-markup', {responseType: 'text'}).toPromise() + try { + return await this.http.get('/custom-login-markup', {responseType: 'text'}).toPromise() + } catch (error) { + return '' + } } } From ba296f6d7bf5124258ef706d9c3b111ff0d474bb Mon Sep 17 00:00:00 2001 From: esurface Date: Wed, 17 Jul 2024 12:28:52 -0400 Subject: [PATCH 69/81] Import and Export the ngx-translate TranslationModule in SharedModule so it can be used internally and externally --- editor/src/app/shared/shared.module.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/editor/src/app/shared/shared.module.ts b/editor/src/app/shared/shared.module.ts index dbec5cc0c8..14307583f4 100644 --- a/editor/src/app/shared/shared.module.ts +++ b/editor/src/app/shared/shared.module.ts @@ -25,15 +25,16 @@ import {MatProgressBarModule} from "@angular/material/progress-bar"; @NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [ - CommonModule, - MatTableModule, - MatMenuModule, - MatIconModule, - MatDialogModule, - MatButtonModule, - MatProgressBarModule - ], + imports: [ + CommonModule, + MatTableModule, + MatMenuModule, + MatIconModule, + MatDialogModule, + MatButtonModule, + MatProgressBarModule, + TranslateModule + ], providers: [ AppConfigService, ServerConfigService, @@ -42,7 +43,6 @@ import {MatProgressBarModule} from "@angular/material/progress-bar"; ProcessGuard ], exports: [ - TranslateModule, DynamicTableComponent, MatSnackBarModule, TangyLoadingComponent, @@ -51,7 +51,8 @@ import {MatProgressBarModule} from "@angular/material/progress-bar"; NgxPermissionsModule, HasAPermissionDirective, HasSomePermissionsDirective, - HasAllPermissionsDirective + HasAllPermissionsDirective, + TranslateModule ], declarations: [ TangyLoadingComponent, From b1824b51bbe8d3c4e9336405b54db5e283279701 Mon Sep 17 00:00:00 2001 From: esurface Date: Wed, 17 Jul 2024 16:28:40 -0400 Subject: [PATCH 70/81] Generate CSV for attendence, score, behavior fix --- server/src/scripts/generate-csv/batch.js | 149 ++++++++++++----------- 1 file changed, 77 insertions(+), 72 deletions(-) diff --git a/server/src/scripts/generate-csv/batch.js b/server/src/scripts/generate-csv/batch.js index 0c5c0bdcc4..aed71dccba 100755 --- a/server/src/scripts/generate-csv/batch.js +++ b/server/src/scripts/generate-csv/batch.js @@ -27,7 +27,6 @@ function getData(dbName, formId, skip, batchSize, year, month) { try { const key = (year && month) ? `${formId}_${year}_${month}` : formId const target = `${dbDefaults.prefix}/${dbName}/_design/tangy-reporting/_view/resultsByGroupFormId?keys=["${key}"]&include_docs=true&skip=${skip}&limit=${limit}` - console.log(target) axios.get(target) .then(response => { resolve(response.data.rows.map(row => row.doc)) @@ -42,14 +41,56 @@ function getData(dbName, formId, skip, batchSize, year, month) { }); } +function handleCSVReplacementAndDisabledFields(value, csvReplacementCharacters) { + // Handle csv-safe character replacement and disabled fields + if (Array.isArray(value)) { + return '' + } + if (typeof value === 'string') { + if (csvReplacementCharacters) { + csvReplacementCharacters.forEach(expression => { + const search = expression["search"]; + const replace = expression["replace"]; + if (search && replace) { + const re = new RegExp(search, 'g') + try { + value = value.replace(re, replace) + } catch (e) { + console.log("ERROR! re: " + re + " replace: " + replace + " value: " + value + " Error: " + e) + } + } + }) + } + } + if (typeof header === 'string' && header.split('.').length === 3) { + const itemId = header.split('.')[1] + if (itemId && doc[`${itemId}_disabled`] === 'true') { + if (outputDisabledFieldsToCSV) { + return value + } else { + return process.env.T_REPORTING_MARK_SKIPPED_WITH + } + } else { + if (value === undefined) { + return process.env.T_REPORTING_MARK_UNDEFINED_WITH + } else { + return value + } + } + } else { + if (value === undefined) { + return process.env.T_REPORTING_MARK_UNDEFINED_WITH + } else { + return value + } + } +} + async function batch() { const state = JSON.parse(await readFile(params.statePath)) - console.log("state.skip: " + state.skip) const docs = await getData(state.dbName, state.formId, state.skip, state.batchSize, state.year, state.month) let outputDisabledFieldsToCSV = state.groupConfigurationDoc? state.groupConfigurationDoc["outputDisabledFieldsToCSV"] : false - console.log("outputDisabledFieldsToCSV: " + outputDisabledFieldsToCSV) let csvReplacementCharacters = state.groupConfigurationDoc? state.groupConfigurationDoc["csvReplacementCharacters"] : false - console.log("csvReplacementCharacters: " + JSON.stringify(csvReplacementCharacters)) // let csvReplacement = csvReplacementCharacters? JSON.parse(csvReplacementCharacters) : false if (docs.length === 0) { state.complete = true @@ -59,85 +100,49 @@ async function batch() { try { rows = [] docs.forEach(doc => { - let row = [doc._id, ...state.headersKeys.map(header => { - // Check to see if variable comes from a section that was disabled. - if (doc.type === 'attendance' && header === 'attendanceList') { - // skip - } else if (doc.type === 'scores' && header === 'scoreList') { - // skip - } else { - let value = doc[header]; - console.log("header: " + header + " value: " + value) - if (typeof value === 'string') { - if (csvReplacementCharacters) { - csvReplacementCharacters.forEach(expression => { - const search = expression["search"]; - const replace = expression["replace"]; - if (search && replace) { - const re = new RegExp(search, 'g') - try { - value = value.replace(re, replace) - } catch (e) { - console.log("ERROR! re: " + re + " replace: " + replace + " value: " + value + " Error: " + e) - } - } - }) - } - } - if (typeof header === 'string' && header.split('.').length === 3) { - console.log("Checking header: " + header + " to see if it is disabled.") - const itemId = header.split('.')[1] - if (itemId && doc[`${itemId}_disabled`] === 'true') { - if (outputDisabledFieldsToCSV) { - return value - } else { - return process.env.T_REPORTING_MARK_SKIPPED_WITH - } - } else { - if (value === undefined) { - return process.env.T_REPORTING_MARK_UNDEFINED_WITH - } else { - return value - } - } - } else { - if (value === undefined) { - return process.env.T_REPORTING_MARK_UNDEFINED_WITH + if (doc.type === 'attendance') { + doc.attendanceList.forEach(listItem => { + let row = ([doc._id, ...state.headersKeys.map(header => { + if (listItem[header]) { + return listItem[header]; } else { - return value + let value = doc[header]; + return handleCSVReplacementAndDisabledFields(value, csvReplacementCharacters); } - } - } - })] - if (doc.type === 'attendance') { - doc.attendanceList.forEach(attendance => { - let row = [doc._id, ...state.headersKeys.map(header => { - let value = attendance[header]; - console.log("header: " + header + " value: " + value) - return value - })] + })]) rows.push(row) }) } else if (doc.type === 'scores') { - doc.scoreList.forEach(score => { - let row = [doc._id, ...state.headersKeys.map(header => { - let value = score[header]; - console.log("header: " + header + " value: " + value) - return value - })] + doc.scoreList.forEach(listItem => { + let row = ([doc._id, ...state.headersKeys.map(header => { + if (listItem[header]) { + return listItem[header]; + } else { + let value = doc[header]; + return handleCSVReplacementAndDisabledFields(value, csvReplacementCharacters); + } + })]) rows.push(row) }) } else if (doc.type === 'behavior') { - doc.studentBehaviorList.forEach(behavior => { - let row = [doc._id, ...state.headersKeys.map(header => { - let value = behavior[header]; - console.log("header: " + header + " value: " + value) - return value - })] + doc.studentBehaviorList.forEach(listItem => { + let row = ([doc._id, ...state.headersKeys.map(header => { + if (listItem[header]) { + return listItem[header]; + } else { + let value = doc[header]; + return handleCSVReplacementAndDisabledFields(value, csvReplacementCharacters); + } + })]) rows.push(row) }) } else { - // rows = docs.map(doc => { + let row = [doc._id, + ...state.headersKeys.map(header => { + let value = doc[header]; + return handleCSVReplacementAndDisabledFields(value, csvReplacementCharacters); + }) + ] rows.push(row) } }) From eaf3ea83375e9ef217fccbe17dfca1d3219753ef Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 18 Jul 2024 14:33:24 -0400 Subject: [PATCH 71/81] Add client/dist to develop.sh --- develop.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/develop.sh b/develop.sh index a7af59e3cb..154a5c64c6 100755 --- a/develop.sh +++ b/develop.sh @@ -221,6 +221,7 @@ OPTIONS="--link $T_COUCHDB_CONTAINER_NAME:couchdb \ --volume $(pwd)/server/package.json:/tangerine/server/package.json:delegated \ --volume $(pwd)/server/src:/tangerine/server/src:delegated \ --volume $(pwd)/client/src:/tangerine/client/src:delegated \ + --volume $(pwd)/client/dist:/tangerine/client/dist:delegated \ --volume $(pwd)/server/reporting:/tangerine/server/reporting:delegated \ --volume $(pwd)/upgrades:/tangerine/upgrades:delegated \ --volume $(pwd)/scripts/generate-csv/bin.js:/tangerine/scripts/generate-csv/bin.js:delegated \ From 52eb6be4e877db74f8c49fb801c1612a5b3a4a41 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 18 Jul 2024 14:33:44 -0400 Subject: [PATCH 72/81] Update debugging-reporting.md --- docs/developer/debugging-reporting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/debugging-reporting.md b/docs/developer/debugging-reporting.md index a1f825ce1d..5d8c97db83 100644 --- a/docs/developer/debugging-reporting.md +++ b/docs/developer/debugging-reporting.md @@ -11,7 +11,7 @@ Summary of steps: 1. Enter the container on command line with `docker exec -it tangerine bash`. 1. Clear reporting cache with command `reporting-cache-clear`. 1. Run a batch with debugger enabled by running command `node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch)`. -1. Latch onto debugging session using Chrome inspect. You may need to click "configure" and add `localhost:9228` to "Target discovery settings". +1. Latch onto debugging session using [Chrome Inspect](chrome://inspect/#devices). You may need to click "configure" and add `localhost:9228` to "Target discovery settings". ## Instructions From c95317d1caea70df1bc75461be46b1557d412eb5 Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 18 Jul 2024 14:35:00 -0400 Subject: [PATCH 73/81] Use variable service to store -processedDocs in import-user-profile --- .../import-user-profile.component.ts | 12 +++++++----- server/src/group-views.js | 2 +- server/src/upgrade/v3.31.0.js | 13 +++++++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100755 server/src/upgrade/v3.31.0.js diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index e3ec22666a..3262fefbed 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -2,10 +2,10 @@ import { Component, ViewChild, ElementRef, AfterContentInit } from '@angular/cor import { HttpClient } from '@angular/common/http'; import { UserService } from '../../shared/_services/user.service'; import { Router } from '@angular/router'; -import PouchDB from 'pouchdb'; import { AppConfigService } from 'src/app/shared/_services/app-config.service'; import { AppConfig } from 'src/app/shared/_classes/app-config.class'; import { _TRANSLATE } from 'src/app/shared/translation-marker'; +import { VariableService } from 'src/app/shared/_services/variable.service'; @Component({ @@ -33,7 +33,8 @@ export class ImportUserProfileComponent implements AfterContentInit { private router: Router, private http: HttpClient, private userService: UserService, - private appConfigService: AppConfigService + private appConfigService: AppConfigService, + private variableService: VariableService ) { } ngAfterContentInit() { @@ -63,7 +64,8 @@ export class ImportUserProfileComponent implements AfterContentInit { await this.userService.saveUserAccount({ ...this.userAccount, userUUID: newUserProfile['_id'], initialProfileComplete: true }) this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?totalRows=true`).toPromise())['totalDocs'] const docsToQuery = 1000; - let processedDocs = +localStorage.getItem(`${username}-processedDocs`) || 0; + let previousProcessedDocs = await this.variableService.get(`${username}-processedDocs`) + let processedDocs = parseInt(previousProcessedDocs) || 0; while (processedDocs < this.totalDocs) { this.docs = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/${docsToQuery}/${processedDocs}`).toPromise() for (let doc of this.docs) { @@ -72,9 +74,9 @@ export class ImportUserProfileComponent implements AfterContentInit { } processedDocs += this.docs.length; this.processedDocs = processedDocs - localStorage.setItem(`${username}-processedDocs`, String(processedDocs)) + await this.variableService.set(`${username}-processedDocs`, String(processedDocs)) } - } else{ + } else { this.state = this.STATE_NOT_FOUND } this.router.navigate([`/${this.appConfig.homeUrl}`] ); diff --git a/server/src/group-views.js b/server/src/group-views.js index db658c92b7..5336486242 100644 --- a/server/src/group-views.js +++ b/server/src/group-views.js @@ -78,7 +78,7 @@ module.exports.responsesByUserProfileShortCode = function(doc) { } module.exports.userProfileByUserProfileShortCode = function (doc) { - if (doc.collection === "TangyFormResponse"&&doc.form && doc.form.id === 'user-profile') { + if (doc.collection === "TangyFormResponse" && doc.form && doc.form.id === 'user-profile') { return emit(doc._id.substr(doc._id.length - 6, doc._id.length), true); } } diff --git a/server/src/upgrade/v3.31.0.js b/server/src/upgrade/v3.31.0.js new file mode 100755 index 0000000000..0ec27efb8e --- /dev/null +++ b/server/src/upgrade/v3.31.0.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +const util = require("util"); +const exec = util.promisify(require('child_process').exec) + +async function go() { + console.log('Updating views with a new view used for the User Profile listing.') + try { + await exec(`/tangerine/server/src/scripts/push-all-groups-views.js `) + } catch (e) { + console.log(e) + } +} +go() From 15cbac9e6e042cd7ee71ec45a667e4f8cd81db1f Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 18 Jul 2024 15:49:29 -0400 Subject: [PATCH 74/81] Simplify user profile short code APIs and reduce views --- .../import-user-profile.component.ts | 6 ++-- server/src/express-app.js | 1 + server/src/group-views.js | 29 ++++--------------- ...up-responses-by-user-profile-short-code.js | 13 ++------- .../group-user-profile-by-short-code.js | 18 ++++++++++++ 5 files changed, 31 insertions(+), 36 deletions(-) create mode 100644 server/src/routes/group-user-profile-by-short-code.js diff --git a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts index 3262fefbed..39a07a0787 100644 --- a/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts +++ b/client/src/app/user-profile/import-user-profile/import-user-profile.component.ts @@ -57,11 +57,11 @@ export class ImportUserProfileComponent implements AfterContentInit { try { this.appConfig = await this.appConfigService.getAppConfig() this.shortCode = this.userShortCodeInput.nativeElement.value; - let newUserProfile = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?userProfile=true`).toPromise() - if(!!newUserProfile){ + let existingUserProfile = await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/userProfileByShortCode/${this.shortCode}`).toPromise() + if(!!existingUserProfile){ const username = this.userService.getCurrentUser() this.state = this.STATE_SYNCING - await this.userService.saveUserAccount({ ...this.userAccount, userUUID: newUserProfile['_id'], initialProfileComplete: true }) + await this.userService.saveUserAccount({ ...this.userAccount, userUUID: existingUserProfile['_id'], initialProfileComplete: true }) this.totalDocs = (await this.http.get(`${this.appConfig.serverUrl}api/${this.appConfig.groupId}/responsesByUserProfileShortCode/${this.shortCode}/?totalRows=true`).toPromise())['totalDocs'] const docsToQuery = 1000; let previousProcessedDocs = await this.variableService.get(`${username}-processedDocs`) diff --git a/server/src/express-app.js b/server/src/express-app.js index ad69f9cd50..56bd30a618 100644 --- a/server/src/express-app.js +++ b/server/src/express-app.js @@ -210,6 +210,7 @@ app.get('/app/:groupId/responsesByMonthAndFormId/:keys/:limit?/:skip?', isAuthen // Note that the lack of security middleware here is intentional. User IDs are UUIDs and thus sufficiently hard to guess. app.get('/api/:groupId/responsesByUserProfileId/:userProfileId/:limit?/:skip?', require('./routes/group-responses-by-user-profile-id.js')) app.get('/api/:groupId/responsesByUserProfileShortCode/:userProfileShortCode/:limit?/:skip?', require('./routes/group-responses-by-user-profile-short-code.js')) +app.get('/api/:groupId/userProfileByShortCode/:userProfileShortCode', require('./routes/group-user-profile-by-short-code.js')) app.get('/api/:groupId/:docId', isAuthenticatedOrHasUploadToken, require('./routes/group-doc-read.js')) app.put('/api/:groupId/:docId', isAuthenticated, require('./routes/group-doc-write.js')) app.post('/api/:groupId/:docId', isAuthenticated, require('./routes/group-doc-write.js')) diff --git a/server/src/group-views.js b/server/src/group-views.js index 5336486242..8b979c3b9e 100644 --- a/server/src/group-views.js +++ b/server/src/group-views.js @@ -59,10 +59,10 @@ module.exports.unpaid = function(doc) { } } -module.exports.responsesByUserProfileShortCode = function(doc) { - if (doc.collection === "TangyFormResponse") { +module.exports.responsesByUserProfileShortCode = { + map: function (doc) { if (doc.form && doc.form.id === 'user-profile') { - return emit(doc._id.substr(doc._id.length-6, doc._id.length), true) + return emit(doc._id.substr(doc._id.length-6, doc._id.length), 1) } var inputs = doc.items.reduce(function(acc, item) { return acc.concat(item.inputs)}, []) var userProfileInput = null @@ -72,9 +72,10 @@ module.exports.responsesByUserProfileShortCode = function(doc) { } }) if (userProfileInput) { - emit(userProfileInput.value.substr(userProfileInput.value.length-6, userProfileInput.value.length), true) + emit(userProfileInput.value.substr(userProfileInput.value.length-6, userProfileInput.value.length), 1) } - } + }, + reduce: '_count' } module.exports.userProfileByUserProfileShortCode = function (doc) { @@ -82,24 +83,6 @@ module.exports.userProfileByUserProfileShortCode = function (doc) { return emit(doc._id.substr(doc._id.length - 6, doc._id.length), true); } } -module.exports.totalDocsByUserProfileShortCode = { - map: function (doc) { - if (doc.form && doc.form.id === 'user-profile') { - return emit(doc._id.substr(doc._id.length-6, doc._id.length), 1) - } - var inputs = doc.items.reduce(function(acc, item) { return acc.concat(item.inputs)}, []) - var userProfileInput = null - inputs.forEach(function(input) { - if (input.name === 'userProfileId') { - userProfileInput = input - } - }) - if (userProfileInput) { - emit(userProfileInput.value.substr(userProfileInput.value.length-6, userProfileInput.value.length), 1) - } - }, - reduce: '_count' -} module.exports.groupIssues = function(doc) { if (doc.collection === "TangyFormResponse" && doc.type === "issue") { diff --git a/server/src/routes/group-responses-by-user-profile-short-code.js b/server/src/routes/group-responses-by-user-profile-short-code.js index 80e6041e6e..19b82593b7 100644 --- a/server/src/routes/group-responses-by-user-profile-short-code.js +++ b/server/src/routes/group-responses-by-user-profile-short-code.js @@ -1,12 +1,11 @@ const DB = require('../db.js') -const clog = require('tangy-log').clog const log = require('tangy-log').log module.exports = async (req, res) => { try { const groupDb = new DB(req.params.groupId) const userProfileShortCode = req.params.userProfileShortCode - let options = { key: userProfileShortCode, include_docs: true } + let options = { key: userProfileShortCode } if (req.params.limit) { options.limit = req.params.limit } @@ -14,16 +13,10 @@ module.exports = async (req, res) => { options.skip = req.params.skip } if (req.query.totalRows) { - const results = await groupDb.query('totalDocsByUserProfileShortCode', { key: userProfileShortCode, limit: 1,skip: 0, include_docs: false, reduce:true, group:true }); + const results = await groupDb.query('responsesByUserProfileShortCode', { key: userProfileShortCode, limit: 1,skip: 0, include_docs: false, reduce: true, group: true }); res.send({ totalDocs: results.rows[0].value }) - } else if (req.query.userProfile) { - await groupDb.query("userProfileByUserProfileShortCode", { limit: 0 }); - const result = await groupDb.query("userProfileByUserProfileShortCode", { key: userProfileShortCode, limit: 1, include_docs: true }); - const profile = result.rows[0] - const data = profile ? {_id: profile.id, key: profile.id, formId: profile.doc.form.id, collection: profile.doc.collection}: undefined - res.send(data) } else { - const results = await groupDb.query('responsesByUserProfileShortCode', options); + const results = await groupDb.query('responsesByUserProfileShortCode', { ...options, include_docs: true, reduce: false }); const docs = results.rows.map(row => row.doc) res.send(docs) } diff --git a/server/src/routes/group-user-profile-by-short-code.js b/server/src/routes/group-user-profile-by-short-code.js new file mode 100644 index 0000000000..a5c08eb36e --- /dev/null +++ b/server/src/routes/group-user-profile-by-short-code.js @@ -0,0 +1,18 @@ +const DB = require('../db.js') +const log = require('tangy-log').log + +module.exports = async (req, res) => { + try { + const groupDb = new DB(req.params.groupId) + const userProfileShortCode = req.params.userProfileShortCode + + const result = await groupDb.query("userProfileByUserProfileShortCode", { key: userProfileShortCode, limit: 1, include_docs: true }); + const profile = result.rows[0] + const data = profile ? {_id: profile.id, key: profile.id, formId: profile.doc.form.id, collection: profile.doc.collection} : undefined + res.send(data) + + } catch (error) { + log.error(error); + res.status(500).send(error); + } +} \ No newline at end of file From e3d311d0414e73cba3efabad73d095bf8f3e7c4b Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 18 Jul 2024 16:58:44 -0400 Subject: [PATCH 75/81] Update editor package to use tangy-form v4.45.0 --- editor/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/package.json b/editor/package.json index 96ef3a359c..0c3241aa9e 100644 --- a/editor/package.json +++ b/editor/package.json @@ -49,7 +49,7 @@ "redux": "^4.0.5", "rxjs": "~6.5.5", "rxjs-compat": "^6.5.5", - "tangy-form": "^4.43.2", + "tangy-form": "^4.45.0", "tangy-form-editor": "7.18.0", "translation-web-component": "0.0.3", "tslib": "^1.11.2", From a6543ad971531b956c267a4db5721ad11fb2d288 Mon Sep 17 00:00:00 2001 From: esurface Date: Fri, 19 Jul 2024 11:59:50 -0400 Subject: [PATCH 76/81] Update editor package to use tangy-form v4.45.1 --- editor/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/package.json b/editor/package.json index 0c3241aa9e..f45a2e1fab 100644 --- a/editor/package.json +++ b/editor/package.json @@ -49,7 +49,7 @@ "redux": "^4.0.5", "rxjs": "~6.5.5", "rxjs-compat": "^6.5.5", - "tangy-form": "^4.45.0", + "tangy-form": "^4.45.1", "tangy-form-editor": "7.18.0", "translation-web-component": "0.0.3", "tslib": "^1.11.2", From 64cd6f1ad5921801a103d306e92bd85c28ec433b Mon Sep 17 00:00:00 2001 From: esurface Date: Mon, 22 Jul 2024 12:23:19 -0400 Subject: [PATCH 77/81] Update CHANGELOG --- CHANGELOG.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d67f3dd5..483572a2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,19 +3,22 @@ ## v3.31.0 __New Features__ -- A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in [tangy-forms](https://github.com/Tangerine-Community/tangy-form/blob/master/CHANGELOG.md#v4430). [#3473](https://github.com/Tangerine-Community/Tangerine/issues/3473) + +- **Audio and Visual Feedback**: A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in [tangy-forms](https://github.com/Tangerine-Community/tangy-form/blob/master/CHANGELOG.md#v4430). [#3473](https://github.com/Tangerine-Community/Tangerine/issues/3473) - Client Login Screen Custom HTML: A new app-config.json setting, `customLoginMarkup`, allows for custom HTML to be added to the login screen. This feature is useful for adding custom branding or additional information to the login screen. As an example: ```json "customLoginMarkup": "
logo
" ``` -- Add ID Search to Data > Import: A new feature in the Data > Import screen allows users to search for a specific ID in the imported data. This feature is useful for finding specific records synced to the server when reviewing or editing completed form responses. [#3681](https://github.com/Tangerine-Community/Tangerine/issues/3681) +- **Improved Data Management**: + * Data Managers now have access to a full workflow to review, edit, and verify data in the Tangerine web server. The Data Manager can click on a record and enter a new screen that allows them to perform actions align with a data collection supervision process. + * Searching has been improved to allow seaqrching for a specific ID in the imported data. This feature is useful for finding specific records synced to the server when reviewing or editing completed form responses. [#3681](https://github.com/Tangerine-Community/Tangerine/issues/3681) __Fixes__ - Client Search Service: exclude archived cases from recent activity -- Improve import of responses using user-profile short code on client [#3696](https://github.com/Tangerine-Community/Tangerine/issues/3696) - Media library cannot upload photos [#3583](https://github.com/Tangerine-Community/Tangerine/issues/3583) +- User Profile Import: The process of importing an existing device user now allows for retries and an asynchronous process to download existing records. This fixes an issue cause by timeouts when trying to import a user with a large number of records. [#3696](https://github.com/Tangerine-Community/Tangerine/issues/3696) __Tangerine Teach__ @@ -24,12 +27,18 @@ __Tangerine Teach__ __Libs and Dependencies__ - Bump version of `tangy-form` to v4.31.1 and `tangy-form-editor` to v7.18.0 for the new Prompt Box widget +- Bump version of `tangy-form` to v4.44.0 for GPS Disabling __Server upgrade instructions__ See the [Server Upgrade Insturctions](https://docs.tangerinecentral.org/system-administrator/upgrade-instructions). -*Special Instructions for this release:* NONE +*Special Instructions for this release:* + +Once the Tangerine and CouchDB are running, run the upgrade script for v3.31.0: + +`docker exec -it tangerine /tangerine/server/src/upgrade/v3.31.0.js` + ## v3.30.2 From 53bbfd69bc12e7db9622ecd21877dc197bd8cd2b Mon Sep 17 00:00:00 2001 From: esurface Date: Mon, 22 Jul 2024 12:26:52 -0400 Subject: [PATCH 78/81] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 483572a2ee..7f36165a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ __Tangerine Teach__ __Libs and Dependencies__ - Bump version of `tangy-form` to v4.31.1 and `tangy-form-editor` to v7.18.0 for the new Prompt Box widget -- Bump version of `tangy-form` to v4.44.0 for GPS Disabling +- Bump version of `tangy-form` to v4.45.1 for disabling of `tangy-gps` in server edits __Server upgrade instructions__ From dde3f059ce20e6ab9bea19b6e470cc4c738dbc30 Mon Sep 17 00:00:00 2001 From: esurface Date: Tue, 23 Jul 2024 13:55:18 -0400 Subject: [PATCH 79/81] Fix addition of tangerineModifiedOn and tangerineModifiedByUserId to mysql-js outputs --- server/src/modules/mysql-js/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/modules/mysql-js/index.js b/server/src/modules/mysql-js/index.js index c42d968e42..29e7a17267 100644 --- a/server/src/modules/mysql-js/index.js +++ b/server/src/modules/mysql-js/index.js @@ -328,6 +328,7 @@ const getItemValue = (doc, variableName) => { const generateFlatResponse = async function (formResponse, sanitized) { const groupId = formResponse.groupId; + // Anything added to this list need to be added to the valuesToRemove list in each of the convert_response function. let flatFormResponse = { _id: formResponse._id, formTitle: formResponse.form.title, @@ -782,7 +783,7 @@ async function convert_response(knex, doc, groupId, tableName) { // # Delete the following keys; - const valuesToRemove = ['_id', '_rev','buildChannel','buildId','caseEventId','deviceId','eventFormId','eventId','groupId','participantId','startDatetime', 'startUnixtime'] + const valuesToRemove = ['_id', '_rev','buildChannel','buildId','caseEventId','deviceId','eventFormId','eventId','groupId','participantId','startDatetime', 'startUnixtime', 'tangerineModifiedOn', 'tangerineModifiedByUserId'] valuesToRemove.forEach(e => delete doc[e]); const cleanData = populateDataFromDocument(doc, data); return cleanData From 385886401d2dc5b3331a15662999563bbf3d6a49 Mon Sep 17 00:00:00 2001 From: esurface Date: Tue, 23 Jul 2024 14:26:02 -0400 Subject: [PATCH 80/81] Remove noisy logs --- server/src/modules/mysql-js/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/modules/mysql-js/index.js b/server/src/modules/mysql-js/index.js index 29e7a17267..54970dba3c 100644 --- a/server/src/modules/mysql-js/index.js +++ b/server/src/modules/mysql-js/index.js @@ -348,7 +348,6 @@ const generateFlatResponse = async function (formResponse, sanitized) { if (formResponse.form.id === '') { formResponse.form.id = 'blank' } - console.log('formResponse.form.id: ' + formResponse.form.id) flatFormResponse['formId'] = formResponse.form.id } @@ -730,7 +729,6 @@ async function convert_response(knex, doc, groupId, tableName) { const caseEventId = doc.caseEventId var formID = doc.formId || data['formid'] - console.log("formID: " + formID) if (formID) { // thanks to https://stackoverflow.com/a/14822579 const find = '-' From 05f797454816469808bd3b1315e7a28cf1a90a7c Mon Sep 17 00:00:00 2001 From: esurface Date: Thu, 25 Jul 2024 14:32:28 -0400 Subject: [PATCH 81/81] When has a list of one or more groups, running will only process the groups in the list --- CHANGELOG.md | 1 + config.defaults.sh | 10 ++++++---- server/src/reporting/clear-reporting-cache.js | 12 ++++++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f36165a98..21ce7e7ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ __Fixes__ - Client Search Service: exclude archived cases from recent activity - Media library cannot upload photos [#3583](https://github.com/Tangerine-Community/Tangerine/issues/3583) - User Profile Import: The process of importing an existing device user now allows for retries and an asynchronous process to download existing records. This fixes an issue cause by timeouts when trying to import a user with a large number of records. [#3696](https://github.com/Tangerine-Community/Tangerine/issues/3696) +- When `T_ONLY_PROCESS_THESE_GROUPS` has a list of one or more groups, running `reporting-cache-clear` will only process the groups in the list __Tangerine Teach__ diff --git a/config.defaults.sh b/config.defaults.sh index f2ac9c8ebd..4e4d551031 100755 --- a/config.defaults.sh +++ b/config.defaults.sh @@ -28,6 +28,8 @@ T_MYSQL_CONTAINER_NAME="mysql" T_MYSQL_USER="admin" T_MYSQL_PASSWORD="password" T_MYSQL_PHPMYADMIN="FALSE" +# Enter "true" if using a mysql container instead of an external database service such as AWS RDS. This will launch a mysql container. +T_USE_MYSQL_CONTAINER="" # # Optional @@ -62,12 +64,12 @@ T_REPORTING_DELAY="300000" # Number of change docs from the Couchdb changes feed queried by reporting-worker (i.e. use as the limit parameter) T_LIMIT_NUMBER_OF_CHANGES=200 -# Limit processing to certain group dbs. +# Limit processing to certain group dbs. Cache clear and batching reporting outputs will only run on the groups specified below. +# If empty, all groups will be processed. +# The value of the paramter is an array of group names. For example: +# T_ONLY_PROCESS_THESE_GROUPS="['group-1','group-2']" T_ONLY_PROCESS_THESE_GROUPS="" -# Enter "true" if using a mysql container instead of an external database service such as AWS RDS. This will launch a mysql container. -T_USE_MYSQL_CONTAINER="" - # When CSV is generated, this determines how many form responses are held in memory during a batch. The higher the number the more memory this process will take but the faster it will complete. T_CSV_BATCH_SIZE=50 diff --git a/server/src/reporting/clear-reporting-cache.js b/server/src/reporting/clear-reporting-cache.js index 071ef04868..0d529440bf 100644 --- a/server/src/reporting/clear-reporting-cache.js +++ b/server/src/reporting/clear-reporting-cache.js @@ -24,7 +24,16 @@ async function clearReportingCache() { if (reportingWorkerRunning) await sleep(1*1000) } console.log('Clearing reporting caches...') - const groupNames = await groupsList() + let groupNames = await groupsList() + + let onlyProcessTheseGroups = [] + if (process.env.T_ONLY_PROCESS_THESE_GROUPS && process.env.T_ONLY_PROCESS_THESE_GROUPS !== '') { + onlyProcessTheseGroups = process.env.T_ONLY_PROCESS_THESE_GROUPS + ? JSON.parse(process.env.T_ONLY_PROCESS_THESE_GROUPS.replace(/\'/g, `"`)) + : [] + } + groupNames = groupNames.filter(groupName => onlyProcessTheseGroups.includes(groupName)); + await tangyModules.hook('clearReportingCache', { groupNames }) // update worker state console.log('Resetting reporting worker state...') @@ -33,7 +42,6 @@ async function clearReportingCache() { const newState = Object.assign({}, state, { databases: state.databases.map(({name, sequence}) => { return {name, sequence: 0}}) }) - console.log("newState: " + JSON.stringify(newState)) await writeFile(REPORTING_WORKER_STATE, JSON.stringify(newState), 'utf-8') await unlink(REPORTING_WORKER_PAUSE) console.log('Done!')