From a6b451e24fe25fd36c7f89c862f193356df10051 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Thu, 14 Jul 2022 15:58:00 +0100 Subject: [PATCH 01/19] fix(appServerService): move appserver methods to appserver service and add schedule methods --- .../services/app-server/app-server.service.ts | 164 +++++++++++++++++- 1 file changed, 160 insertions(+), 4 deletions(-) diff --git a/src/app/core/services/app-server/app-server.service.ts b/src/app/core/services/app-server/app-server.service.ts index 83ea1beeb..aee71b4d9 100644 --- a/src/app/core/services/app-server/app-server.service.ts +++ b/src/app/core/services/app-server/app-server.service.ts @@ -1,4 +1,9 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http' +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, + HttpResponse +} from '@angular/common/http' import { Injectable } from '@angular/core' import * as moment from 'moment-timezone' import * as urljoin from 'url-join' @@ -9,6 +14,12 @@ import { } from '../../../../assets/data/defaultConfig' import { ConfigKeys } from '../../../shared/enums/config' import { StorageKeys } from '../../../shared/enums/storage' +import { + FcmNotificationDto, + FcmNotificationError +} from '../../../shared/models/app-server' +import { SingleNotification } from '../../../shared/models/notification-handler' +import { Task } from '../../../shared/models/task' import { RemoteConfigService } from '../config/remote-config.service' import { SubjectConfigService } from '../config/subject-config.service' import { LocalizationService } from '../misc/localization.service' @@ -22,6 +33,9 @@ export class AppServerService { SUBJECT_PATH = 'users' PROJECT_PATH = 'projects' GITHUB_CONTENT_PATH = 'github/content' + QUESTIONNAIRE_SCHEDULE_PATH = 'questionnaire/schedule' + NOTIFICATIONS_PATH = 'messaging/notifications' + STATE_EVENTS_PATH = 'state_events' constructor( public storage: StorageService, @@ -45,8 +59,8 @@ export class AppServerService { this.getFCMToken() ]) ) - .then(([subjectId, projectId, enrolmentDate, attributes, fcmToken]) => - this.addProjectIfMissing(projectId).then(() => + .then(([subjectId, projectId, enrolmentDate, attributes, fcmToken]) => { + return this.addProjectIfMissing(projectId).then(() => this.addSubjectIfMissing( subjectId, projectId, @@ -55,7 +69,7 @@ export class AppServerService { fcmToken ) ) - ) + }) } getHeaders() { @@ -210,6 +224,148 @@ export class AppServerService { }) } + getSchedule(): Promise { + return Promise.all([ + this.subjectConfig.getParticipantLogin(), + this.subjectConfig.getProjectName() + ]).then(([subjectId, projectId]) => { + return this.getHeaders() + .then(headers => + this.http + .get( + urljoin( + this.APP_SERVER_URL, + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH + ), + { headers } + ) + .toPromise() + ) + .then((tasks: Task[]) => + tasks.map(t => Object.assign(t, { timestamp: t.timestamp * 1000 })) + ) + }) + } + + generateSchedule(): Promise { + return Promise.all([ + this.subjectConfig.getParticipantLogin(), + this.subjectConfig.getProjectName() + ]).then(([subjectId, projectId]) => { + return this.getHeaders().then(headers => + this.http + .post( + urljoin( + this.APP_SERVER_URL, + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH + ), + { headers } + ) + .toPromise() + ) + }) + } + + pullAllPublishedNotifications(subject) { + return this.getHeaders().then(headers => + this.http + .get( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + subject.projectId, + this.SUBJECT_PATH, + subject.subjectId, + this.NOTIFICATIONS_PATH + ), + { headers } + ) + .toPromise() + ) + } + + deleteNotification(subject, notification: SingleNotification) { + return this.getHeaders().then(headers => + this.http + .delete( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + subject.projectId, + this.SUBJECT_PATH, + subject.subjectId, + this.NOTIFICATIONS_PATH, + notification.id.toString() + ), + { headers } + ) + .toPromise() + ) + } + + updateNotificationState(subject, notificationId, state) { + return this.getHeaders().then(headers => + this.http + .post( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + subject.projectId, + this.SUBJECT_PATH, + subject.subjectId, + this.NOTIFICATIONS_PATH, + notificationId.toString(), + this.STATE_EVENTS_PATH + ), + { notificationId: notificationId, state: state, time: new Date() }, + { headers } + ) + .toPromise() + ) + } + + public addNotification(notification, subjectId, projectId): Promise { + return this.getHeaders().then(headers => + this.http + .post( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.NOTIFICATIONS_PATH + ), + notification.notificationDto, + { headers, observe: 'response' } + ) + .toPromise() + .then((res: HttpResponse) => { + this.logger.log('Successfully sent! Updating notification Id') + return res.body + }) + .catch((err: HttpErrorResponse) => { + this.logger.log('Http request returned an error: ' + err.message) + const data: FcmNotificationError = err.error + if (err.status == 409) { + this.logger.log( + 'Notification already exists, storing notification data..' + ) + return data.dto ? data.dto : notification.notification + } + return this.logger.error('Failed to send notification', err) + }) + ) + } + getFCMToken() { return this.storage.get(StorageKeys.FCM_TOKEN) } From 5524ff747a2f72c03bbbcd6421eb6c36c96c7213 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Thu, 14 Jul 2022 15:58:36 +0100 Subject: [PATCH 02/19] fix: move appserver methods to appserver service --- .../fcm-rest-notification.service.ts | 135 ++++-------------- 1 file changed, 27 insertions(+), 108 deletions(-) diff --git a/src/app/core/services/notifications/fcm-rest-notification.service.ts b/src/app/core/services/notifications/fcm-rest-notification.service.ts index 55f471178..75f9fb0d6 100644 --- a/src/app/core/services/notifications/fcm-rest-notification.service.ts +++ b/src/app/core/services/notifications/fcm-rest-notification.service.ts @@ -40,7 +40,6 @@ export class FcmRestNotificationService extends FcmNotificationService { NOTIFICATIONS_PATH = 'messaging/notifications' SUBJECT_PATH = 'users' PROJECT_PATH = 'projects' - STATE_EVENTS_PATH = 'state_events' resumeListener: Subscription = new Subscription() @@ -55,7 +54,6 @@ export class FcmRestNotificationService extends FcmNotificationService { public remoteConfig: RemoteConfigService, public localization: LocalizationService, private appServerService: AppServerService, - private http: HttpClient, private webIntent: WebIntent ) { super(storage, config, firebase, platform, logger, remoteConfig) @@ -85,17 +83,19 @@ export class FcmRestNotificationService extends FcmNotificationService { tasks, messageId ) - return this.updateNotificationState( - subject, - notification.id, - NotificationMessagingState.DELIVERED - ).then(() => - this.updateNotificationState( + return this.appServerService + .updateNotificationState( subject, notification.id, - NotificationMessagingState.OPENED + NotificationMessagingState.DELIVERED + ) + .then(() => + this.appServerService.updateNotificationState( + subject, + notification.id, + NotificationMessagingState.OPENED + ) ) - ) }) }) } @@ -123,16 +123,14 @@ export class FcmRestNotificationService extends FcmNotificationService { this.logger.log('NOTIFICATIONS Scheduling FCM notifications') this.logger.log(fcmNotifications) return Promise.all( - fcmNotifications - .map(n => - this.sendNotification(n, subject.subjectId, subject.projectId) - ) - .concat([this.setLastNotificationUpdate(Date.now())]) + fcmNotifications.map(n => + this.sendNotification(n, subject.subjectId, subject.projectId) + ) ) }) } - publishTestNotification(subject): Promise { + publishTestNotification(subject): Promise { return this.sendNotification( this.format(this.notifications.createTestNotification(), subject), subject.subjectId, @@ -140,22 +138,21 @@ export class FcmRestNotificationService extends FcmNotificationService { ) } - pullAllPublishedNotifications(subject) { + sendNotification(notification, subjectId, projectId) { return this.appServerService - .getHeaders() - .then(headers => - this.http - .get( - this.getNotificationEndpoint(subject.projectId, subject.subjectId), - { headers } - ) - .toPromise() - ) + .addNotification(notification, subjectId, projectId) + .then((resultNotification: FcmNotificationDto) => { + this.setLastNotificationUpdate(Date.now()) + notification.notification.id = resultNotification.id + return (notification.notification.messageId = + resultNotification.fcmMessageId) + }) } cancelAllNotifications(subject): Promise { - return this.pullAllPublishedNotifications(subject).then( - (res: FcmNotifications) => { + return this.appServerService + .pullAllPublishedNotifications(subject) + .then((res: FcmNotifications) => { const now = Date.now() const notifications = res.notifications .map(n => ({ @@ -164,26 +161,13 @@ export class FcmRestNotificationService extends FcmNotificationService { })) .filter(n => n.timestamp > now) notifications.map(o => this.cancelSingleNotification(subject, o)) - } - ) + }) } cancelSingleNotification(subject, notification: SingleNotification) { if (notification.id) { return this.appServerService - .getHeaders() - .then(headers => - this.http - .delete( - this.getNotificationEndpoint( - subject.projectId, - subject.subjectId, - notification.id - ), - { headers } - ) - .toPromise() - ) + .deleteNotification(subject, notification) .then(() => { this.logger.log('Success cancelling notification ' + notification.id) return (notification.id = undefined) @@ -194,59 +178,6 @@ export class FcmRestNotificationService extends FcmNotificationService { } } - updateNotificationState(subject, notificationId, state) { - return this.appServerService - .getHeaders() - .then(headers => - this.http - .post( - urljoin( - this.getNotificationEndpoint( - subject.projectId, - subject.subjectId, - notificationId - ), - this.STATE_EVENTS_PATH - ), - { notificationId: notificationId, state: state, time: new Date() }, - { headers } - ) - .toPromise() - ) - } - - private sendNotification(notification, subjectId, projectId): Promise { - return this.appServerService.getHeaders().then(headers => - this.http - .post( - this.getNotificationEndpoint(projectId, subjectId), - notification.notificationDto, - { headers, observe: 'response' } - ) - .toPromise() - .then((res: HttpResponse) => { - this.logger.log('Successfully sent! Updating notification Id') - return res.body - }) - .catch((err: HttpErrorResponse) => { - this.logger.log('Http request returned an error: ' + err.message) - const data: FcmNotificationError = err.error - if (err.status == 409) { - this.logger.log( - 'Notification already exists, storing notification data..' - ) - return data.dto ? data.dto : notification.notification - } - return this.logger.error('Failed to send notification', err) - }) - .then((resultNotification: FcmNotificationDto) => { - notification.notification.id = resultNotification.id - return (notification.notification.messageId = - resultNotification.fcmMessageId) - }) - ) - } - private format(notification: SingleNotification, subject) { const taskInfo = notification.task return { @@ -267,16 +198,4 @@ export class FcmRestNotificationService extends FcmNotificationService { } } } - - getNotificationEndpoint(projectId, subjectId, notificationId?) { - return urljoin( - this.appServerService.getAppServerURL(), - this.PROJECT_PATH, - projectId, - this.SUBJECT_PATH, - subjectId, - this.NOTIFICATIONS_PATH, - notificationId ? notificationId.toString() : '' - ) - } } From 8db88bc43d8fb0d0b30f329fb08556a75507d325 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Thu, 14 Jul 2022 16:08:08 +0100 Subject: [PATCH 03/19] fix(schedule): pull schedule from appserver for schedule generation --- .../schedule/schedule-generator.service.ts | 2 +- .../services/schedule/schedule.service.ts | 42 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/app/core/services/schedule/schedule-generator.service.ts b/src/app/core/services/schedule/schedule-generator.service.ts index d68647b64..9edbcee02 100644 --- a/src/app/core/services/schedule/schedule-generator.service.ts +++ b/src/app/core/services/schedule/schedule-generator.service.ts @@ -152,7 +152,7 @@ export class ScheduleGeneratorService { completionWindow ): Task { const task: Task = this.util.deepCopy(DefaultTask) - task.index = index + task.id = index task.timestamp = timestamp task.name = assessment.name task.type = assessment.type diff --git a/src/app/core/services/schedule/schedule.service.ts b/src/app/core/services/schedule/schedule.service.ts index efb18e83e..7eda7c1e0 100755 --- a/src/app/core/services/schedule/schedule.service.ts +++ b/src/app/core/services/schedule/schedule.service.ts @@ -10,6 +10,7 @@ import { getMilliseconds, setDateTimeToMidnightEpoch } from '../../../shared/utilities/time' +import { AppServerService } from '../app-server/app-server.service' import { LogService } from '../misc/log.service' import { StorageService } from '../storage/storage.service' import { ScheduleGeneratorService } from './schedule-generator.service' @@ -27,7 +28,8 @@ export class ScheduleService { constructor( private storage: StorageService, private schedule: ScheduleGeneratorService, - private logger: LogService + private logger: LogService, + private appServer: AppServerService ) {} getTasks(type: AssessmentType): Promise { @@ -141,20 +143,30 @@ export class ScheduleService { generateSchedule(referenceTimestamp, utcOffsetPrev) { this.logger.log('Updating schedule..', referenceTimestamp) - return this.getCompletedTasks() - .then(completedTasks => { - return this.schedule.runScheduler( - referenceTimestamp, - completedTasks, - utcOffsetPrev - ) + return this.appServer + .init() + .then(() => this.appServer.getSchedule()) + .then(res => { + if (res.length) { + return this.setTasks(AssessmentType.SCHEDULED, res) + } else this.appServer.generateSchedule() + }) + .catch(() => { + return this.getCompletedTasks() + .then(completedTasks => { + return this.schedule.runScheduler( + referenceTimestamp, + completedTasks, + utcOffsetPrev + ) + }) + .then(res => + Promise.all([ + this.setTasks(AssessmentType.SCHEDULED, res.schedule), + this.setCompletedTasks(res.completed ? res.completed : []) + ]) + ) }) - .then(res => - Promise.all([ - this.setTasks(AssessmentType.SCHEDULED, res.schedule), - this.setCompletedTasks(res.completed ? res.completed : []) - ]) - ) } generateSingleAssessmentTask( @@ -181,7 +193,7 @@ export class ScheduleService { const type = task.type return this.getTasks(type).then(tasks => { if (!tasks) return - const updatedTasks = tasks.map(d => (d.index === task.index ? task : d)) + const updatedTasks = tasks.map(d => (d.id === task.id ? task : d)) return this.setTasks(type, updatedTasks) }) } From fd07e2b60033e16d25041e81bbde8c272dd658d7 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Thu, 14 Jul 2022 16:17:01 +0100 Subject: [PATCH 04/19] fix: task interface --- src/app/shared/models/task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/models/task.ts b/src/app/shared/models/task.ts index 561ae6c5f..4913fb523 100755 --- a/src/app/shared/models/task.ts +++ b/src/app/shared/models/task.ts @@ -2,7 +2,7 @@ import { AssessmentType } from './assessment' import { SingleNotification } from './notification-handler' export interface Task { - index: number + id: number completed: boolean reportedCompletion: boolean timestamp: number From d8eb62d669293e63a33f576395a14d97de79faeb Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Fri, 15 Jul 2022 17:40:45 +0100 Subject: [PATCH 05/19] fix: processing of tasks after app server pull --- .../services/app-server/app-server.service.ts | 34 ++++++++----------- .../schedule/schedule-generator.service.ts | 29 +++++++++------- .../services/schedule/schedule.service.ts | 30 ++++++++-------- src/app/pages/home/services/tasks.service.ts | 18 +++++----- .../questions/services/questions.service.ts | 10 ------ 5 files changed, 58 insertions(+), 63 deletions(-) diff --git a/src/app/core/services/app-server/app-server.service.ts b/src/app/core/services/app-server/app-server.service.ts index aee71b4d9..800a099a7 100644 --- a/src/app/core/services/app-server/app-server.service.ts +++ b/src/app/core/services/app-server/app-server.service.ts @@ -229,25 +229,21 @@ export class AppServerService { this.subjectConfig.getParticipantLogin(), this.subjectConfig.getProjectName() ]).then(([subjectId, projectId]) => { - return this.getHeaders() - .then(headers => - this.http - .get( - urljoin( - this.APP_SERVER_URL, - this.PROJECT_PATH, - projectId, - this.SUBJECT_PATH, - subjectId, - this.QUESTIONNAIRE_SCHEDULE_PATH - ), - { headers } - ) - .toPromise() - ) - .then((tasks: Task[]) => - tasks.map(t => Object.assign(t, { timestamp: t.timestamp * 1000 })) - ) + return this.getHeaders().then(headers => + this.http + .get( + urljoin( + this.APP_SERVER_URL, + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH + ), + { headers } + ) + .toPromise() + ) }) } diff --git a/src/app/core/services/schedule/schedule-generator.service.ts b/src/app/core/services/schedule/schedule-generator.service.ts index 9edbcee02..b827e3241 100644 --- a/src/app/core/services/schedule/schedule-generator.service.ts +++ b/src/app/core/services/schedule/schedule-generator.service.ts @@ -46,7 +46,7 @@ export class ScheduleGeneratorService { refTimestamp, completedTasks: Task[], utcOffsetPrev - ): Promise { + ): Promise { return Promise.all([ this.questionnaire.getAssessments(AssessmentType.SCHEDULED), this.fetchScheduleYearCoverage() @@ -63,20 +63,25 @@ export class ScheduleGeneratorService { }, [] ) - // NOTE: Check for completed tasks - const res = this.updateScheduleWithCompletedTasks( - schedule, - completedTasks, - utcOffsetPrev - ) - this.logger.log('[√] Updated task schedule.') - return Promise.resolve({ - schedule: res.schedule.sort(compareTasks), - completed: res.completed - }) + return schedule }) } + mapTaskDTO(task: Task, assesmentType: AssessmentType): Promise { + return this.questionnaire + .getAssessmentForTask(assesmentType, task) + .then(assessment => { + const newTask = Object.assign(task, { + timestamp: new Date(task.timestamp).getTime(), + nQuestions: assessment.questions.length, + warning: this.localization.chooseText(assessment.warn), + requiresInClinicCompletion: assessment.requiresInClinicCompletion, + notifications: [] + }) + return newTask + }) + } + getProtocolValues(protocol, type, defaultRefTime) { // repeatProtocol/repeatP - This repeats the protocol until the end of the year coverage (default: 3 years). // - This specifices the reference timestamp from which to generate the individual tasks. diff --git a/src/app/core/services/schedule/schedule.service.ts b/src/app/core/services/schedule/schedule.service.ts index 7eda7c1e0..e64ed67dd 100755 --- a/src/app/core/services/schedule/schedule.service.ts +++ b/src/app/core/services/schedule/schedule.service.ts @@ -143,30 +143,32 @@ export class ScheduleService { generateSchedule(referenceTimestamp, utcOffsetPrev) { this.logger.log('Updating schedule..', referenceTimestamp) - return this.appServer - .init() - .then(() => this.appServer.getSchedule()) - .then(res => { - if (res.length) { - return this.setTasks(AssessmentType.SCHEDULED, res) - } else this.appServer.generateSchedule() - }) - .catch(() => { - return this.getCompletedTasks() - .then(completedTasks => { + return Promise.all([this.appServer.init(), this.getCompletedTasks()]).then( + ([, completedTasks]) => { + return this.appServer + .getSchedule() + .then(tasks => tasks.map(this.schedule.mapTaskDTO)) + .catch(() => { return this.schedule.runScheduler( referenceTimestamp, completedTasks, utcOffsetPrev ) }) - .then(res => + .then((schedule: Task[]) => { + // NOTE: Check for completed tasks + const res = this.schedule.updateScheduleWithCompletedTasks( + schedule, + completedTasks, + utcOffsetPrev + ) Promise.all([ this.setTasks(AssessmentType.SCHEDULED, res.schedule), this.setCompletedTasks(res.completed ? res.completed : []) ]) - ) - }) + }) + } + ) } generateSingleAssessmentTask( diff --git a/src/app/pages/home/services/tasks.service.ts b/src/app/pages/home/services/tasks.service.ts index 5e99d6b24..65f3e0f9e 100644 --- a/src/app/pages/home/services/tasks.service.ts +++ b/src/app/pages/home/services/tasks.service.ts @@ -62,15 +62,17 @@ export class TasksService { } getSortedTasksOfToday(): Promise> { - return this.getTasksOfToday().then(tasks => { - const sortedTasks = new Map() - tasks.forEach(t => { - const midnight = setDateTimeToMidnightEpoch(new Date(t.timestamp)) - if (sortedTasks.has(midnight)) sortedTasks.get(midnight).push(t) - else sortedTasks.set(midnight, [t]) + return this.getTasksOfToday() + .then(t => t.sort((a, b) => a.timestamp - b.timestamp)) + .then(tasks => { + const sortedTasks = new Map() + tasks.forEach(t => { + const midnight = setDateTimeToMidnightEpoch(new Date(t.timestamp)) + if (sortedTasks.has(midnight)) sortedTasks.get(midnight).push(t) + else sortedTasks.set(midnight, [t]) + }) + return sortedTasks }) - return sortedTasks - }) } getTaskProgress(): Promise { diff --git a/src/app/pages/questions/services/questions.service.ts b/src/app/pages/questions/services/questions.service.ts index f67c6e256..fbd1e1e84 100644 --- a/src/app/pages/questions/services/questions.service.ts +++ b/src/app/pages/questions/services/questions.service.ts @@ -258,16 +258,6 @@ export class QuestionsService { return this.finish.createClinicalFollowUpTask(assessment) } - getHiddenQuestions(): Promise { - return this.remoteConfig - .read() - .then(config => - config.getOrDefault(ConfigKeys.QUESTIONS_HIDDEN, DefaultQuestionsHidden) - ) - .then(res => JSON.parse(res)) - .catch(e => DefaultQuestionsHidden) - } - stringToArray(array, delimiter) { return array.split(delimiter).map(s => s.trim()) } From a05d7cdb3d7fe048caf446dd44fe712a5e3f6c25 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Thu, 21 Jul 2022 21:51:29 +0100 Subject: [PATCH 06/19] fix: separate AppServer Schedule service and Local Schedule service --- .../schedule/appserver-schedule.service.ts | 87 +++++++++++++++++++ .../schedule/local-schedule.service.ts | 77 ++++++++++++++++ .../schedule/schedule-factory.service.ts | 72 +++++++++++++++ .../schedule/schedule-generator.service.ts | 17 +++- .../services/schedule/schedule.service.ts | 83 +++--------------- src/app/shared/enums/config.ts | 1 + 6 files changed, 264 insertions(+), 73 deletions(-) create mode 100644 src/app/core/services/schedule/appserver-schedule.service.ts create mode 100644 src/app/core/services/schedule/local-schedule.service.ts create mode 100644 src/app/core/services/schedule/schedule-factory.service.ts mode change 100755 => 100644 src/app/core/services/schedule/schedule.service.ts diff --git a/src/app/core/services/schedule/appserver-schedule.service.ts b/src/app/core/services/schedule/appserver-schedule.service.ts new file mode 100644 index 000000000..47043cbd4 --- /dev/null +++ b/src/app/core/services/schedule/appserver-schedule.service.ts @@ -0,0 +1,87 @@ +import {} from './notification.service' + +import { Injectable } from '@angular/core' + +import { Assessment, AssessmentType } from '../../../shared/models/assessment' +import { Task } from '../../../shared/models/task' +import { compareTasks } from '../../../shared/utilities/compare-tasks' +import { + getMilliseconds, + setDateTimeToMidnightEpoch +} from '../../../shared/utilities/time' +import { AppServerService } from '../app-server/app-server.service' +import { QuestionnaireService } from '../config/questionnaire.service' +import { LocalizationService } from '../misc/localization.service' +import { LogService } from '../misc/log.service' +import { StorageService } from '../storage/storage.service' +import { ScheduleGeneratorService } from './schedule-generator.service' +import { ScheduleService } from './schedule.service' + +@Injectable() +export class AppserverScheduleService extends ScheduleService { + constructor( + private store: StorageService, + logger: LogService, + private appServer: AppServerService, + private localization: LocalizationService, + private questionnaire: QuestionnaireService + ) { + super(store, logger) + } + + getTasksForDate(date: Date, type: AssessmentType) { + return this.getTasks(type).then(schedule => { + const startTime = setDateTimeToMidnightEpoch(date) + const endTime = startTime + getMilliseconds({ days: 1 }) + return schedule + ? schedule.filter(d => { + return ( + d.timestamp + d.completionWindow > startTime && + d.timestamp < endTime + ) + }) + : [] + }) + } + + generateSchedule(referenceTimestamp, utcOffsetPrev) { + this.logger.log('Updating schedule..', referenceTimestamp) + return Promise.all([this.appServer.init(), this.getCompletedTasks()]).then( + ([, completedTasks]) => { + return this.appServer + .getSchedule() + .then(tasks => + tasks.map(t => this.mapTaskDTO(t, AssessmentType.SCHEDULED)) + ) + .then((schedule: Task[]) => { + console.log(schedule) + // TODO: Check for completed tasks + return schedule + }) + } + ) + } + + generateSingleAssessmentTask( + assessment: Assessment, + assessmentType, + referenceDate: number + ) { + return + } + + mapTaskDTO(task: Task, assesmentType: AssessmentType): Promise { + return this.questionnaire + .getAssessmentForTask(assesmentType, task) + .then(assessment => { + const newTask = Object.assign(task, { + timestamp: getMilliseconds({ seconds: task.timestamp }), + nQuestions: assessment.questions.length, + warning: this.localization.chooseText(assessment.warn), + requiresInClinicCompletion: assessment.requiresInClinicCompletion, + notifications: [] + }) + return newTask + }) + } +} diff --git a/src/app/core/services/schedule/local-schedule.service.ts b/src/app/core/services/schedule/local-schedule.service.ts new file mode 100644 index 000000000..e3f6aa7cb --- /dev/null +++ b/src/app/core/services/schedule/local-schedule.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core' + +import { Assessment, AssessmentType } from '../../../shared/models/assessment' +import { Task } from '../../../shared/models/task' +import { compareTasks } from '../../../shared/utilities/compare-tasks' +import { + getMilliseconds, + setDateTimeToMidnightEpoch +} from '../../../shared/utilities/time' +import { LogService } from '../misc/log.service' +import { StorageService } from '../storage/storage.service' +import { ScheduleGeneratorService } from './schedule-generator.service' +import { ScheduleService } from './schedule.service' + +@Injectable() +export class LocalScheduleService extends ScheduleService { + constructor( + private store: StorageService, + logger: LogService, + private scheduleGenerator: ScheduleGeneratorService + ) { + super(store, logger) + } + + getTasksForDate(date: Date, type: AssessmentType) { + return this.getTasks(type).then(schedule => { + const startTime = setDateTimeToMidnightEpoch(date) + const endTime = startTime + getMilliseconds({ days: 1 }) + return schedule + ? schedule.filter(d => { + return ( + d.timestamp + d.completionWindow > startTime && + d.timestamp < endTime + ) + }) + : [] + }) + } + + generateSchedule(referenceTimestamp, utcOffsetPrev) { + this.logger.log('Updating schedule..', referenceTimestamp) + return this.getCompletedTasks() + .then(completedTasks => { + return this.scheduleGenerator.runScheduler( + referenceTimestamp, + completedTasks, + utcOffsetPrev + ) + }) + .then(res => + Promise.all([ + this.setTasks(AssessmentType.SCHEDULED, res.schedule), + this.setCompletedTasks(res.completed ? res.completed : []) + ]) + ) + } + + generateSingleAssessmentTask( + assessment: Assessment, + assessmentType, + referenceDate: number + ) { + return this.getTasks(assessmentType).then((tasks: Task[]) => { + const schedule = this.scheduleGenerator.buildTasksForSingleAssessment( + assessment, + tasks ? tasks.length : 0, + referenceDate, + assessmentType + ) + const newTasks = (tasks ? tasks.concat(schedule) : schedule).sort( + compareTasks + ) + this.changeDetectionEmitter.emit() + return this.setTasks(assessmentType, newTasks) + }) + } +} diff --git a/src/app/core/services/schedule/schedule-factory.service.ts b/src/app/core/services/schedule/schedule-factory.service.ts new file mode 100644 index 000000000..00b1042e1 --- /dev/null +++ b/src/app/core/services/schedule/schedule-factory.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@angular/core' + +import { DefaultScheduleServiceType } from '../../../../assets/data/defaultConfig' +import { ConfigKeys } from '../../../shared/enums/config' +import { Assessment, AssessmentType } from '../../../shared/models/assessment' +import { SchedulerType } from '../../../shared/models/notification-handler' +import { RemoteConfigService } from '../config/remote-config.service' +import { LogService } from '../misc/log.service' +import { StorageService } from '../storage/storage.service' +import { AppserverScheduleService } from './appserver-schedule.service' +import { LocalScheduleService } from './local-schedule.service' +import { ScheduleService } from './schedule.service' + +@Injectable() +export class ScheduleFactoryService extends ScheduleService { + scheduleService: ScheduleService + + constructor( + public localScheduleService: LocalScheduleService, + public appServerScheduleSerice: AppserverScheduleService, + private remoteConfig: RemoteConfigService, + private store: StorageService, + logger: LogService + ) { + super(store, logger) + } + + init() { + return this.remoteConfig + .forceFetch() + .then(config => + config.getOrDefault( + ConfigKeys.SCHEDULE_SERVICE_TYPE, + DefaultScheduleServiceType + ) + ) + .then(type => { + type = SchedulerType.LOCAL + switch (type) { + case SchedulerType.LOCAL: + return (this.scheduleService = this.localScheduleService) + case SchedulerType.APPSERVER: + return (this.scheduleService = this.appServerScheduleSerice) + default: + throw new Error('No such scheduling service available') + } + }) + } + + generateSchedule(referenceTimestamp, utcOffsetPrev) { + return this.scheduleService.generateSchedule( + referenceTimestamp, + utcOffsetPrev + ) + } + + generateSingleAssessmentTask( + assessment: Assessment, + assessmentType, + referenceDate: number + ) { + return this.generateSingleAssessmentTask( + assessment, + assessmentType, + referenceDate + ) + } + + getTasksForDate(date: Date, type: AssessmentType) { + return this.getTasksForDate(date, type) + } +} diff --git a/src/app/core/services/schedule/schedule-generator.service.ts b/src/app/core/services/schedule/schedule-generator.service.ts index b827e3241..da6e86469 100644 --- a/src/app/core/services/schedule/schedule-generator.service.ts +++ b/src/app/core/services/schedule/schedule-generator.service.ts @@ -17,6 +17,7 @@ import { Task } from '../../../shared/models/task' import { compareTasks } from '../../../shared/utilities/compare-tasks' import { advanceRepeat, + getMilliseconds, setDateTimeToMidnight, setDateTimeToMidnightEpoch, timeIntervalToMillis @@ -46,7 +47,7 @@ export class ScheduleGeneratorService { refTimestamp, completedTasks: Task[], utcOffsetPrev - ): Promise { + ): Promise { return Promise.all([ this.questionnaire.getAssessments(AssessmentType.SCHEDULED), this.fetchScheduleYearCoverage() @@ -63,7 +64,17 @@ export class ScheduleGeneratorService { }, [] ) - return schedule + // NOTE: Check for completed tasks + const res = this.updateScheduleWithCompletedTasks( + schedule, + completedTasks, + utcOffsetPrev + ) + this.logger.log('[√] Updated task schedule.') + return Promise.resolve({ + schedule: res.schedule.sort(compareTasks), + completed: res.completed + }) }) } @@ -72,7 +83,7 @@ export class ScheduleGeneratorService { .getAssessmentForTask(assesmentType, task) .then(assessment => { const newTask = Object.assign(task, { - timestamp: new Date(task.timestamp).getTime(), + timestamp: getMilliseconds({ seconds: task.timestamp }), nQuestions: assessment.questions.length, warning: this.localization.chooseText(assessment.warn), requiresInClinicCompletion: assessment.requiresInClinicCompletion, diff --git a/src/app/core/services/schedule/schedule.service.ts b/src/app/core/services/schedule/schedule.service.ts old mode 100755 new mode 100644 index e64ed67dd..b52a2f44f --- a/src/app/core/services/schedule/schedule.service.ts +++ b/src/app/core/services/schedule/schedule.service.ts @@ -16,7 +16,7 @@ import { StorageService } from '../storage/storage.service' import { ScheduleGeneratorService } from './schedule-generator.service' @Injectable() -export class ScheduleService { +export abstract class ScheduleService { private readonly SCHEDULE_STORE = { SCHEDULE_TASKS: StorageKeys.SCHEDULE_TASKS, SCHEDULE_TASKS_ON_DEMAND: StorageKeys.SCHEDULE_TASKS_ON_DEMAND, @@ -26,12 +26,20 @@ export class ScheduleService { changeDetectionEmitter: EventEmitter = new EventEmitter() constructor( - private storage: StorageService, - private schedule: ScheduleGeneratorService, - private logger: LogService, - private appServer: AppServerService + protected storage: StorageService, + protected logger: LogService ) {} + abstract generateSchedule(referenceTimestamp, utcOffsetPrev) + + abstract generateSingleAssessmentTask( + assessment: Assessment, + assessmentType, + referenceDate: number + ) + + abstract getTasksForDate(date: Date, type: AssessmentType) + getTasks(type: AssessmentType): Promise { switch (type) { case AssessmentType.SCHEDULED: @@ -59,21 +67,6 @@ export class ScheduleService { } } - getTasksForDate(date: Date, type: AssessmentType) { - return this.getTasks(type).then(schedule => { - const startTime = setDateTimeToMidnightEpoch(date) - const endTime = startTime + getMilliseconds({ days: 1 }) - return schedule - ? schedule.filter(d => { - return ( - d.timestamp + d.completionWindow > startTime && - d.timestamp < endTime - ) - }) - : [] - }) - } - getScheduledTasks(): Promise { return this.storage.get(this.SCHEDULE_STORE.SCHEDULE_TASKS) } @@ -141,56 +134,6 @@ export class ScheduleService { return this.storage.push(this.SCHEDULE_STORE.SCHEDULE_TASKS_COMPLETED, task) } - generateSchedule(referenceTimestamp, utcOffsetPrev) { - this.logger.log('Updating schedule..', referenceTimestamp) - return Promise.all([this.appServer.init(), this.getCompletedTasks()]).then( - ([, completedTasks]) => { - return this.appServer - .getSchedule() - .then(tasks => tasks.map(this.schedule.mapTaskDTO)) - .catch(() => { - return this.schedule.runScheduler( - referenceTimestamp, - completedTasks, - utcOffsetPrev - ) - }) - .then((schedule: Task[]) => { - // NOTE: Check for completed tasks - const res = this.schedule.updateScheduleWithCompletedTasks( - schedule, - completedTasks, - utcOffsetPrev - ) - Promise.all([ - this.setTasks(AssessmentType.SCHEDULED, res.schedule), - this.setCompletedTasks(res.completed ? res.completed : []) - ]) - }) - } - ) - } - - generateSingleAssessmentTask( - assessment: Assessment, - assessmentType, - referenceDate: number - ) { - return this.getTasks(assessmentType).then((tasks: Task[]) => { - const schedule = this.schedule.buildTasksForSingleAssessment( - assessment, - tasks ? tasks.length : 0, - referenceDate, - assessmentType - ) - const newTasks = (tasks ? tasks.concat(schedule) : schedule).sort( - compareTasks - ) - this.changeDetectionEmitter.emit() - return this.setTasks(assessmentType, newTasks) - }) - } - insertTask(task): Promise { const type = task.type return this.getTasks(type).then(tasks => { diff --git a/src/app/shared/enums/config.ts b/src/app/shared/enums/config.ts index f56e30af2..15cf53d02 100644 --- a/src/app/shared/enums/config.ts +++ b/src/app/shared/enums/config.ts @@ -13,6 +13,7 @@ export class ConfigKeys { static NOTIFICATION_MESSAGING_TYPE = new ConfigKeys( 'notification_messaging_type' ) + static SCHEDULE_SERVICE_TYPE = new ConfigKeys('schedule_service_type') static APP_SERVER_URL = new ConfigKeys('app_server_url') static ON_DEMAND_ASSESSMENT_LABEL = new ConfigKeys( 'on_demand_assessment_label' From 11cdb64a385fed099acb81bc30ef38e0ec2dc221 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Fri, 22 Jul 2022 15:28:13 +0100 Subject: [PATCH 07/19] fix(scheduleService): initialisation and imports --- .../services/notifications/notification-factory.service.ts | 1 + src/app/core/services/schedule/schedule-factory.service.ts | 5 +++-- src/app/core/services/schedule/schedule.service.ts | 2 ++ src/app/pages/pages.module.ts | 7 ++++++- src/app/shared/models/notification-handler.ts | 5 +++++ src/assets/data/defaultConfig.ts | 5 ++++- 6 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/app/core/services/notifications/notification-factory.service.ts b/src/app/core/services/notifications/notification-factory.service.ts index 8782cfa01..531006684 100644 --- a/src/app/core/services/notifications/notification-factory.service.ts +++ b/src/app/core/services/notifications/notification-factory.service.ts @@ -36,6 +36,7 @@ export class NotificationFactoryService extends NotificationService { ) ) .then(type => { + type = NotificationMessagingType.FCM_REST switch (type) { case NotificationMessagingType.LOCAL: return (this.notificationService = this.localNotificationService) diff --git a/src/app/core/services/schedule/schedule-factory.service.ts b/src/app/core/services/schedule/schedule-factory.service.ts index 00b1042e1..ad31df9d2 100644 --- a/src/app/core/services/schedule/schedule-factory.service.ts +++ b/src/app/core/services/schedule/schedule-factory.service.ts @@ -23,6 +23,7 @@ export class ScheduleFactoryService extends ScheduleService { logger: LogService ) { super(store, logger) + this.init() } init() { @@ -59,7 +60,7 @@ export class ScheduleFactoryService extends ScheduleService { assessmentType, referenceDate: number ) { - return this.generateSingleAssessmentTask( + return this.scheduleService.generateSingleAssessmentTask( assessment, assessmentType, referenceDate @@ -67,6 +68,6 @@ export class ScheduleFactoryService extends ScheduleService { } getTasksForDate(date: Date, type: AssessmentType) { - return this.getTasksForDate(date, type) + return this.scheduleService.getTasksForDate(date, type) } } diff --git a/src/app/core/services/schedule/schedule.service.ts b/src/app/core/services/schedule/schedule.service.ts index b52a2f44f..293a6761b 100644 --- a/src/app/core/services/schedule/schedule.service.ts +++ b/src/app/core/services/schedule/schedule.service.ts @@ -30,6 +30,8 @@ export abstract class ScheduleService { protected logger: LogService ) {} + abstract init() + abstract generateSchedule(referenceTimestamp, utcOffsetPrev) abstract generateSingleAssessmentTask( diff --git a/src/app/pages/pages.module.ts b/src/app/pages/pages.module.ts index d1a840f7d..8bb9ecf92 100644 --- a/src/app/pages/pages.module.ts +++ b/src/app/pages/pages.module.ts @@ -19,6 +19,9 @@ import { MessageHandlerService } from '../core/services/notifications/message-ha import { NotificationFactoryService } from '../core/services/notifications/notification-factory.service' import { NotificationGeneratorService } from '../core/services/notifications/notification-generator.service' import { NotificationService } from '../core/services/notifications/notification.service' +import { AppserverScheduleService } from '../core/services/schedule/appserver-schedule.service' +import { LocalScheduleService } from '../core/services/schedule/local-schedule.service' +import { ScheduleFactoryService } from '../core/services/schedule/schedule-factory.service' import { ScheduleGeneratorService } from '../core/services/schedule/schedule-generator.service' import { ScheduleService } from '../core/services/schedule/schedule.service' import { StorageService } from '../core/services/storage/storage.service' @@ -61,7 +64,9 @@ import { SplashModule } from './splash/splash.module' TokenService, KafkaService, LocalizationService, - ScheduleService, + { provide: ScheduleService, useClass: ScheduleFactoryService }, + LocalScheduleService, + AppserverScheduleService, ScheduleGeneratorService, StorageService, TranslatePipe, diff --git a/src/app/shared/models/notification-handler.ts b/src/app/shared/models/notification-handler.ts index ad901bfe2..ec1f22f25 100644 --- a/src/app/shared/models/notification-handler.ts +++ b/src/app/shared/models/notification-handler.ts @@ -39,6 +39,11 @@ export enum NotificationMessagingType { FCM_REST = 'FCM_REST' } +export enum SchedulerType { + LOCAL = 'LOCAL', + APPSERVER = 'APPSERVER' +} + export enum NotificationMessagingState { DELIVERED = 'DELIVERED', OPENED = 'OPENED' diff --git a/src/assets/data/defaultConfig.ts b/src/assets/data/defaultConfig.ts index bcf67b33b..b6d5a03a5 100755 --- a/src/assets/data/defaultConfig.ts +++ b/src/assets/data/defaultConfig.ts @@ -89,7 +89,7 @@ export const DefaultESMCompletionWindow = 600000 // *Default sample task export const DefaultTask: Task = { - index: 0, + id: 0, type: AssessmentType.SCHEDULED, completed: false, reportedCompletion: false, @@ -118,6 +118,9 @@ export const DefaultScheduleVersion = '0.3.10' // *Default max number of completion logs to send on app start export const DefaultNumberOfCompletionLogsToSend = 10 +// *Default schedule service type (either 'LOCAL' or 'APPSERVER') +export const DefaultScheduleServiceType: string = 'LOCAL' + // DEFAULT NOTIFICATION SETUP // *Default notification type (either 'FCM_XMPP', 'FCM_REST' or 'LOCAL' notifications) From aef181c819faf7d5482e0516a6d06c82fc9a68bb Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Tue, 26 Jul 2022 11:42:21 +0100 Subject: [PATCH 08/19] fix: schedule services --- src/app/core/services/schedule/appserver-schedule.service.ts | 2 ++ src/app/core/services/schedule/local-schedule.service.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/app/core/services/schedule/appserver-schedule.service.ts b/src/app/core/services/schedule/appserver-schedule.service.ts index 47043cbd4..ee8fdd8db 100644 --- a/src/app/core/services/schedule/appserver-schedule.service.ts +++ b/src/app/core/services/schedule/appserver-schedule.service.ts @@ -29,6 +29,8 @@ export class AppserverScheduleService extends ScheduleService { super(store, logger) } + init() {} + getTasksForDate(date: Date, type: AssessmentType) { return this.getTasks(type).then(schedule => { const startTime = setDateTimeToMidnightEpoch(date) diff --git a/src/app/core/services/schedule/local-schedule.service.ts b/src/app/core/services/schedule/local-schedule.service.ts index e3f6aa7cb..72285e1ce 100644 --- a/src/app/core/services/schedule/local-schedule.service.ts +++ b/src/app/core/services/schedule/local-schedule.service.ts @@ -22,6 +22,8 @@ export class LocalScheduleService extends ScheduleService { super(store, logger) } + init() {} + getTasksForDate(date: Date, type: AssessmentType) { return this.getTasks(type).then(schedule => { const startTime = setDateTimeToMidnightEpoch(date) From 9c72a7158cdcb36da577b60ac9b5cffc6503b977 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Thu, 4 Aug 2022 16:20:08 +0100 Subject: [PATCH 09/19] fix: update marking task state as completed and fix pulling tasks for certain time window --- .../services/app-server/app-server.service.ts | 62 +++++++++++++++++++ .../schedule/appserver-schedule.service.ts | 40 ++++++------ .../schedule/schedule-factory.service.ts | 5 +- .../home/containers/home-page.component.ts | 2 +- src/app/pages/home/services/tasks.service.ts | 11 ++-- src/app/shared/models/protocol.ts | 8 +++ src/app/shared/models/task.ts | 1 + 7 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/app/core/services/app-server/app-server.service.ts b/src/app/core/services/app-server/app-server.service.ts index 800a099a7..d7ced6e27 100644 --- a/src/app/core/services/app-server/app-server.service.ts +++ b/src/app/core/services/app-server/app-server.service.ts @@ -34,6 +34,8 @@ export class AppServerService { PROJECT_PATH = 'projects' GITHUB_CONTENT_PATH = 'github/content' QUESTIONNAIRE_SCHEDULE_PATH = 'questionnaire/schedule' + QUESTIONNAIRE_TASK = 'questionnaire/task' + QUESTIONNAIRE_STATE_EVENTS_PATH = 'state_events' NOTIFICATIONS_PATH = 'messaging/notifications' STATE_EVENTS_PATH = 'state_events' @@ -247,6 +249,35 @@ export class AppServerService { }) } + getScheduleForDates(startTime: Date, endTime: Date): Promise { + return Promise.all([ + this.subjectConfig.getParticipantLogin(), + this.subjectConfig.getProjectName() + ]).then(([subjectId, projectId]) => { + return this.getHeaders().then(headers => + this.http + .get( + urljoin( + this.APP_SERVER_URL, + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH + ), + { + headers, + params: { + startTime: startTime.toISOString(), + endTime: endTime.toISOString() + } + } + ) + .toPromise() + ) + }) + } + generateSchedule(): Promise { return Promise.all([ this.subjectConfig.getParticipantLogin(), @@ -307,6 +338,37 @@ export class AppServerService { ) } + updateTaskState(taskId, state) { + return Promise.all([ + this.subjectConfig.getParticipantLogin(), + this.subjectConfig.getProjectName() + ]).then(([subjectId, projectId]) => { + return this.getHeaders().then(headers => + this.http + .post( + urljoin( + this.getAppServerURL(), + this.PROJECT_PATH, + projectId, + this.SUBJECT_PATH, + subjectId, + this.QUESTIONNAIRE_SCHEDULE_PATH, + taskId.toString(), + this.QUESTIONNAIRE_STATE_EVENTS_PATH + ), + { + taskId: taskId, + state: state, + time: new Date(), + associatedInfo: '' + }, + { headers } + ) + .toPromise() + ) + }) + } + updateNotificationState(subject, notificationId, state) { return this.getHeaders().then(headers => this.http diff --git a/src/app/core/services/schedule/appserver-schedule.service.ts b/src/app/core/services/schedule/appserver-schedule.service.ts index ee8fdd8db..8e9c7353d 100644 --- a/src/app/core/services/schedule/appserver-schedule.service.ts +++ b/src/app/core/services/schedule/appserver-schedule.service.ts @@ -1,12 +1,15 @@ import {} from './notification.service' import { Injectable } from '@angular/core' +import * as moment from 'moment' import { Assessment, AssessmentType } from '../../../shared/models/assessment' +import { TaskState } from '../../../shared/models/protocol' import { Task } from '../../../shared/models/task' -import { compareTasks } from '../../../shared/utilities/compare-tasks' import { + advanceRepeat, getMilliseconds, + setDateTimeToMidnight, setDateTimeToMidnightEpoch } from '../../../shared/utilities/time' import { AppServerService } from '../app-server/app-server.service' @@ -32,18 +35,15 @@ export class AppserverScheduleService extends ScheduleService { init() {} getTasksForDate(date: Date, type: AssessmentType) { - return this.getTasks(type).then(schedule => { - const startTime = setDateTimeToMidnightEpoch(date) - const endTime = startTime + getMilliseconds({ days: 1 }) - return schedule - ? schedule.filter(d => { - return ( - d.timestamp + d.completionWindow > startTime && - d.timestamp < endTime - ) - }) - : [] - }) + const startTime = setDateTimeToMidnight(date) + const endTime = moment(startTime).add(1, 'days').toDate() + return this.appServer + .getScheduleForDates(startTime, endTime) + .then(tasks => + Promise.all( + tasks.map(t => this.mapTaskDTO(t, AssessmentType.SCHEDULED)) + ) + ) } generateSchedule(referenceTimestamp, utcOffsetPrev) { @@ -53,17 +53,18 @@ export class AppserverScheduleService extends ScheduleService { return this.appServer .getSchedule() .then(tasks => - tasks.map(t => this.mapTaskDTO(t, AssessmentType.SCHEDULED)) + Promise.all( + tasks.map(t => this.mapTaskDTO(t, AssessmentType.SCHEDULED)) + ) ) - .then((schedule: Task[]) => { - console.log(schedule) - // TODO: Check for completed tasks - return schedule - }) } ) } + updateTaskToComplete(updatedTask): Promise { + return this.appServer.updateTaskState(updatedTask.id, TaskState.COMPLETED) + } + generateSingleAssessmentTask( assessment: Assessment, assessmentType, @@ -77,7 +78,6 @@ export class AppserverScheduleService extends ScheduleService { .getAssessmentForTask(assesmentType, task) .then(assessment => { const newTask = Object.assign(task, { - timestamp: getMilliseconds({ seconds: task.timestamp }), nQuestions: assessment.questions.length, warning: this.localization.chooseText(assessment.warn), requiresInClinicCompletion: assessment.requiresInClinicCompletion, diff --git a/src/app/core/services/schedule/schedule-factory.service.ts b/src/app/core/services/schedule/schedule-factory.service.ts index ad31df9d2..14f17ead5 100644 --- a/src/app/core/services/schedule/schedule-factory.service.ts +++ b/src/app/core/services/schedule/schedule-factory.service.ts @@ -36,7 +36,6 @@ export class ScheduleFactoryService extends ScheduleService { ) ) .then(type => { - type = SchedulerType.LOCAL switch (type) { case SchedulerType.LOCAL: return (this.scheduleService = this.localScheduleService) @@ -70,4 +69,8 @@ export class ScheduleFactoryService extends ScheduleService { getTasksForDate(date: Date, type: AssessmentType) { return this.scheduleService.getTasksForDate(date, type) } + + updateTaskToComplete(updatedTask): Promise { + return this.scheduleService.updateTaskToComplete(updatedTask) + } } diff --git a/src/app/pages/home/containers/home-page.component.ts b/src/app/pages/home/containers/home-page.component.ts index 0445e943e..2fbe6c5f0 100755 --- a/src/app/pages/home/containers/home-page.component.ts +++ b/src/app/pages/home/containers/home-page.component.ts @@ -94,7 +94,7 @@ export class HomePageComponent implements OnDestroy { } init() { - this.sortedTasks = this.tasksService.getSortedTasksOfToday() + this.sortedTasks = this.tasksService.getValidTasksMap() this.tasks = this.tasksService.getTasksOfToday() this.currentDate = this.tasksService.getCurrentDateMidnight() this.tasksProgress = this.tasksService.getTaskProgress() diff --git a/src/app/pages/home/services/tasks.service.ts b/src/app/pages/home/services/tasks.service.ts index 65f3e0f9e..a73ba3917 100644 --- a/src/app/pages/home/services/tasks.service.ts +++ b/src/app/pages/home/services/tasks.service.ts @@ -54,14 +54,11 @@ export class TasksService { getTasksOfToday() { return this.schedule .getTasksForDate(new Date(), AssessmentType.SCHEDULED) - .then(tasks => - tasks.filter( - t => !this.isTaskExpired(t) || this.wasTaskCompletedToday(t) - ) - ) + .then(tasks => tasks.filter(t => !this.isTaskExpired(t))) } - getSortedTasksOfToday(): Promise> { + getValidTasksMap(): Promise> { + // This groups the tasks valid for today into a Map, where the key is midnight epoch and the value is an array of tasks return this.getTasksOfToday() .then(t => t.sort((a, b) => a.timestamp - b.timestamp)) .then(tasks => { @@ -109,7 +106,7 @@ export class TasksService { // NOTE: This checks if completion window has passed or task is complete return ( task.timestamp + task.completionWindow < new Date().getTime() || - task.completed + (task.completed && !this.wasTaskCompletedToday(task)) ) } diff --git a/src/app/shared/models/protocol.ts b/src/app/shared/models/protocol.ts index 98479d7cd..0dc0d3ccb 100755 --- a/src/app/shared/models/protocol.ts +++ b/src/app/shared/models/protocol.ts @@ -29,6 +29,14 @@ export enum ReferenceTimestampFormat { NOW = 'now' } +export enum TaskState { + ADDED = 'ADDED', + UPDATED = 'UPDATED', + CANCELLED = 'CANCELLED', + SCHEDULED = 'SCHEDULED', + COMPLETED = 'COMPLETED' +} + export interface ProtocolMetaData { protocol: string url?: string diff --git a/src/app/shared/models/task.ts b/src/app/shared/models/task.ts index 4913fb523..beac7390e 100755 --- a/src/app/shared/models/task.ts +++ b/src/app/shared/models/task.ts @@ -19,6 +19,7 @@ export interface Task { isDemo: boolean order: number isLastTask?: boolean + status?: string } export interface TasksProgress { From cf2767940e2c1069b5fde12442e7e53e5a736963 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Mon, 15 Aug 2022 17:40:57 +0100 Subject: [PATCH 10/19] fix: allow delayed completion status updates to the appserver in case of connectivity issues --- .../schedule/appserver-schedule.service.ts | 13 +++++++++---- .../schedule/schedule-generator.service.ts | 15 --------------- .../core/services/schedule/schedule.service.ts | 10 ++++++++++ .../questions/services/finish-task.service.ts | 5 +++-- .../splash/containers/splash-page.component.ts | 4 +++- src/app/pages/splash/services/splash.service.ts | 8 ++++++++ 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/app/core/services/schedule/appserver-schedule.service.ts b/src/app/core/services/schedule/appserver-schedule.service.ts index 8e9c7353d..dfbb60dee 100644 --- a/src/app/core/services/schedule/appserver-schedule.service.ts +++ b/src/app/core/services/schedule/appserver-schedule.service.ts @@ -40,9 +40,9 @@ export class AppserverScheduleService extends ScheduleService { return this.appServer .getScheduleForDates(startTime, endTime) .then(tasks => - Promise.all( + Promise.all( tasks.map(t => this.mapTaskDTO(t, AssessmentType.SCHEDULED)) - ) + ).then(res => this.setTasks(AssessmentType.SCHEDULED, res)) ) } @@ -53,16 +53,20 @@ export class AppserverScheduleService extends ScheduleService { return this.appServer .getSchedule() .then(tasks => - Promise.all( + Promise.all( tasks.map(t => this.mapTaskDTO(t, AssessmentType.SCHEDULED)) ) ) + .then(res => this.setTasks(AssessmentType.SCHEDULED, res)) } ) } updateTaskToComplete(updatedTask): Promise { - return this.appServer.updateTaskState(updatedTask.id, TaskState.COMPLETED) + return this.appServer + .updateTaskState(updatedTask.id, TaskState.COMPLETED) + .then(() => super.updateTaskToReportedCompletion(updatedTask)) + .catch(() => super.updateTaskToComplete(updatedTask)) } generateSingleAssessmentTask( @@ -78,6 +82,7 @@ export class AppserverScheduleService extends ScheduleService { .getAssessmentForTask(assesmentType, task) .then(assessment => { const newTask = Object.assign(task, { + reportedCompletion: false, nQuestions: assessment.questions.length, warning: this.localization.chooseText(assessment.warn), requiresInClinicCompletion: assessment.requiresInClinicCompletion, diff --git a/src/app/core/services/schedule/schedule-generator.service.ts b/src/app/core/services/schedule/schedule-generator.service.ts index da6e86469..d23f3a254 100644 --- a/src/app/core/services/schedule/schedule-generator.service.ts +++ b/src/app/core/services/schedule/schedule-generator.service.ts @@ -78,21 +78,6 @@ export class ScheduleGeneratorService { }) } - mapTaskDTO(task: Task, assesmentType: AssessmentType): Promise { - return this.questionnaire - .getAssessmentForTask(assesmentType, task) - .then(assessment => { - const newTask = Object.assign(task, { - timestamp: getMilliseconds({ seconds: task.timestamp }), - nQuestions: assessment.questions.length, - warning: this.localization.chooseText(assessment.warn), - requiresInClinicCompletion: assessment.requiresInClinicCompletion, - notifications: [] - }) - return newTask - }) - } - getProtocolValues(protocol, type, defaultRefTime) { // repeatProtocol/repeatP - This repeats the protocol until the end of the year coverage (default: 3 years). // - This specifices the reference timestamp from which to generate the individual tasks. diff --git a/src/app/core/services/schedule/schedule.service.ts b/src/app/core/services/schedule/schedule.service.ts index 293a6761b..d96188a3a 100644 --- a/src/app/core/services/schedule/schedule.service.ts +++ b/src/app/core/services/schedule/schedule.service.ts @@ -97,6 +97,16 @@ export abstract class ScheduleService { }) } + getReportedIncompleteTasks(): Promise { + // These tasks have been completed but have not yet been reported as complete to the app server + return this.getTasks(AssessmentType.ALL).then(tasks => { + const now = new Date().getTime() + return tasks + .filter(d => d.completed && !d.reportedCompletion) + .slice(0, 100) + }) + } + setTasks(type: AssessmentType, tasks: Task[]): Promise { const uniqueTasks = [ ...new Map( diff --git a/src/app/pages/questions/services/finish-task.service.ts b/src/app/pages/questions/services/finish-task.service.ts index 70fc00c96..a80cf5be5 100644 --- a/src/app/pages/questions/services/finish-task.service.ts +++ b/src/app/pages/questions/services/finish-task.service.ts @@ -21,8 +21,9 @@ export class FinishTaskService { updateTaskToComplete(task): Promise { return Promise.all([ - this.schedule.updateTaskToComplete(task), - this.schedule.updateTaskToReportedCompletion(task), + this.schedule + .updateTaskToComplete(task) + .then(res => this.schedule.updateTaskToReportedCompletion(task)), task.type == AssessmentType.SCHEDULED ? this.schedule.addToCompletedTasks(task) : Promise.resolve() diff --git a/src/app/pages/splash/containers/splash-page.component.ts b/src/app/pages/splash/containers/splash-page.component.ts index 8389cf562..0163a3440 100644 --- a/src/app/pages/splash/containers/splash-page.component.ts +++ b/src/app/pages/splash/containers/splash-page.component.ts @@ -53,7 +53,9 @@ export class SplashPageComponent { this.status = this.localization.translateKey( LocKeys.SPLASH_STATUS_SENDING_LOGS ) - return this.splashService.sendMissedQuestionnaireLogs() + return this.splashService + .sendMissedQuestionnaireLogs() + .then(() => this.splashService.sendReportedIncompleteTasks()) }) .catch(e => this.showFetchConfigFail(e)) .then(() => this.navCtrl.setRoot(HomePageComponent)) diff --git a/src/app/pages/splash/services/splash.service.ts b/src/app/pages/splash/services/splash.service.ts index a0e43bf77..266d7c34c 100644 --- a/src/app/pages/splash/services/splash.service.ts +++ b/src/app/pages/splash/services/splash.service.ts @@ -67,4 +67,12 @@ export class SplashService { ) ) } + + sendReportedIncompleteTasks() { + return this.schedule + .getReportedIncompleteTasks() + .then(tasks => + Promise.all(tasks.map(task => this.schedule.updateTaskToComplete(task))) + ) + } } From c39158a5c67388df5bd2166a49635632c5f7ba4f Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Tue, 23 Aug 2022 16:44:04 +0200 Subject: [PATCH 11/19] Ensure that FCM token gets propagated when it is updated --- .../services/app-server/app-server.service.ts | 34 ++++++++----- .../notifications/fcm-notification.service.ts | 48 ++++++++++++++----- .../fcm-rest-notification.service.ts | 4 +- .../core/services/storage/storage.service.ts | 25 +++++++++- 4 files changed, 83 insertions(+), 28 deletions(-) diff --git a/src/app/core/services/app-server/app-server.service.ts b/src/app/core/services/app-server/app-server.service.ts index 83ea1beeb..67c98e3a1 100644 --- a/src/app/core/services/app-server/app-server.service.ts +++ b/src/app/core/services/app-server/app-server.service.ts @@ -15,6 +15,8 @@ import { LocalizationService } from '../misc/localization.service' import { LogService } from '../misc/log.service' import { StorageService } from '../storage/storage.service' import { TokenService } from '../token/token.service' +import { filter } from "rxjs/operators"; +import { Subscription } from "rxjs"; @Injectable() export class AppServerService { @@ -22,6 +24,7 @@ export class AppServerService { SUBJECT_PATH = 'users' PROJECT_PATH = 'projects' GITHUB_CONTENT_PATH = 'github/content' + private tokenSubscription: Subscription = null constructor( public storage: StorageService, @@ -35,7 +38,7 @@ export class AppServerService { init() { // NOTE: Initialising ensures project and subject exists in the app server - return Promise.all([this.updateAppServerURL()]) + return this.updateAppServerURL() .then(() => Promise.all([ this.subjectConfig.getParticipantLogin(), @@ -46,16 +49,25 @@ export class AppServerService { ]) ) .then(([subjectId, projectId, enrolmentDate, attributes, fcmToken]) => - this.addProjectIfMissing(projectId).then(() => - this.addSubjectIfMissing( - subjectId, - projectId, - enrolmentDate, - attributes, - fcmToken - ) - ) - ) + this.addProjectIfMissing(projectId) + .then(() => this.addSubjectIfMissing( + subjectId, + projectId, + enrolmentDate, + attributes, + fcmToken + ) + ).then(httpRes => { + if (this.tokenSubscription !== null) { + this.tokenSubscription.unsubscribe(); + } + this.tokenSubscription = this.storage.observe(StorageKeys.FCM_TOKEN) + .pipe(filter(t => t && t !== fcmToken)) + .subscribe(newFcmToken => + this.addSubjectIfMissing(subjectId, projectId, enrolmentDate, attributes, newFcmToken)) + return httpRes; + }) + ); } getHeaders() { diff --git a/src/app/core/services/notifications/fcm-notification.service.ts b/src/app/core/services/notifications/fcm-notification.service.ts index 9370a7772..c24efe5aa 100644 --- a/src/app/core/services/notifications/fcm-notification.service.ts +++ b/src/app/core/services/notifications/fcm-notification.service.ts @@ -16,6 +16,7 @@ import { SubjectConfigService } from '../config/subject-config.service' import { LogService } from '../misc/log.service' import { StorageService } from '../storage/storage.service' import { NotificationService } from './notification.service' +import { Subscription } from "rxjs"; declare var FirebasePlugin @@ -24,6 +25,7 @@ export abstract class FcmNotificationService extends NotificationService { FCM_TOKEN: string upstreamResends: number ttlMinutes = 10 + private tokenSubscription: Subscription constructor( public store: StorageService, @@ -34,6 +36,7 @@ export abstract class FcmNotificationService extends NotificationService { public remoteConfig: RemoteConfigService ) { super(store) + this.tokenSubscription = null this.platform.ready().then(() => { this.remoteConfig.subject().subscribe(cfg => { cfg @@ -50,21 +53,34 @@ export abstract class FcmNotificationService extends NotificationService { } init() { - this.firebase.setAutoInitEnabled(true) if (!this.platform.is('ios')) FirebasePlugin.setDeliveryMetricsExportToBigQuery(true) - FirebasePlugin.setSenderId( - FCMPluginProjectSenderId, - () => this.logger.log('[NOTIFICATION SERVICE] Set sender id success'), - error => { + return Promise.all([ + this.firebase.setAutoInitEnabled(true), + this.setSenderIdPromise(), + ]) + .then(() => this.firebase.getToken()) + .then(token => { + if (this.tokenSubscription === null) { + this.tokenSubscription = this.firebase + .onTokenRefresh() + .subscribe(t => this.onTokenRefresh(t)) + } + if (token) { + return this.onTokenRefresh(token); + } + }) + } + + setSenderIdPromise(): Promise { + return new Promise((resolve, reject) => + FirebasePlugin.setSenderId(FCMPluginProjectSenderId, resolve, reject)) + .then(() => this.logger.log('[NOTIFICATION SERVICE] Set sender id success')) + .catch(error => { this.logger.error('Failed to set sender ID', error) alert(error) - } - ) - this.firebase - .onTokenRefresh() - .subscribe(token => this.onTokenRefresh(token)) - this.firebase.getToken().then(token => this.onTokenRefresh(token)) + throw error + }) } publish( @@ -113,6 +129,10 @@ export abstract class FcmNotificationService extends NotificationService { } unregisterFromNotifications(): Promise { + if (this.tokenSubscription) { + this.tokenSubscription.unsubscribe(); + this.tokenSubscription = null; + } // NOTE: This will delete the current device token and stop receiving notifications return this.firebase .setAutoInitEnabled(false) @@ -122,8 +142,10 @@ export abstract class FcmNotificationService extends NotificationService { onTokenRefresh(token) { if (token) { this.FCM_TOKEN = token - this.setFCMToken(token) - this.logger.log('[NOTIFICATION SERVICE] Refresh token success') + return this.setFCMToken(token) + .then(() => this.logger.log('[NOTIFICATION SERVICE] Refresh token success')) + } else { + return Promise.resolve() } } diff --git a/src/app/core/services/notifications/fcm-rest-notification.service.ts b/src/app/core/services/notifications/fcm-rest-notification.service.ts index 55f471178..a108c680c 100644 --- a/src/app/core/services/notifications/fcm-rest-notification.service.ts +++ b/src/app/core/services/notifications/fcm-rest-notification.service.ts @@ -68,8 +68,8 @@ export class FcmRestNotificationService extends FcmNotificationService { } init() { - super.init() - return this.appServerService.init() + return super.init() + .then(() => this.appServerService.init()) } onAppOpen() { diff --git a/src/app/core/services/storage/storage.service.ts b/src/app/core/services/storage/storage.service.ts index 4fb37454e..ced96fac7 100755 --- a/src/app/core/services/storage/storage.service.ts +++ b/src/app/core/services/storage/storage.service.ts @@ -1,18 +1,21 @@ import { Injectable } from '@angular/core' import { Storage } from '@ionic/storage' -import { throwError as observableThrowError } from 'rxjs' +import { Observable, Subject, throwError as observableThrowError } from 'rxjs' import { StorageKeys } from '../../../shared/enums/storage' import { LogService } from '../misc/log.service' +import { filter, map, startWith } from "rxjs/operators"; @Injectable() export class StorageService { global: { [key: string]: any } = {} + private readonly keyUpdates: Subject constructor(private storage: Storage, private logger: LogService) { this.prepare().then(() => this.logger.log('Global configuration', this.global) ) + this.keyUpdates = new Subject(); } getStorageState() { @@ -23,15 +26,23 @@ export class StorageService { const k = key.toString() this.global[k] = value return this.storage.set(k, value) + .then(res => { + this.keyUpdates.next(key); + return res; + }); } push(key: StorageKeys, value: any): Promise { if (this.global[key.toString()]) this.global[key.toString()].push(value) else this.global[key.toString()] = [value] return this.storage.set(key.toString(), this.global[key.toString()]) + .then(res => { + this.keyUpdates.next(key); + return res; + }); } - get(key: StorageKeys) { + get(key: StorageKeys): Promise { const k = key.toString() const local = this.global[k] if (local !== undefined) { @@ -44,12 +55,21 @@ export class StorageService { } } + observe(key: StorageKeys): Observable { + return this.keyUpdates.pipe( + startWith(key), + filter(k => k === key || k === null), + map(k => this.global[k.toString()]), + ); + } + remove(key: StorageKeys) { const k = key.toString() return this.storage .remove(k) .then(res => { this.global[k] = null + this.keyUpdates.next(key); return res }) .catch(error => this.handleError(error)) @@ -72,6 +92,7 @@ export class StorageService { clear() { this.global = {} return this.storage.clear() + .then(() => this.keyUpdates.next(null)); } private handleError(error: any) { From dad8e22176d43c39cd07fff2c066233060e3e8de Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Tue, 23 Aug 2022 16:52:26 +0200 Subject: [PATCH 12/19] Ensure that any backlog values in storage are also returned --- src/app/core/services/storage/storage.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/services/storage/storage.service.ts b/src/app/core/services/storage/storage.service.ts index ced96fac7..33f6656a3 100755 --- a/src/app/core/services/storage/storage.service.ts +++ b/src/app/core/services/storage/storage.service.ts @@ -4,7 +4,7 @@ import { Observable, Subject, throwError as observableThrowError } from 'rxjs' import { StorageKeys } from '../../../shared/enums/storage' import { LogService } from '../misc/log.service' -import { filter, map, startWith } from "rxjs/operators"; +import { filter, startWith, switchMap } from "rxjs/operators"; @Injectable() export class StorageService { @@ -59,7 +59,7 @@ export class StorageService { return this.keyUpdates.pipe( startWith(key), filter(k => k === key || k === null), - map(k => this.global[k.toString()]), + switchMap(k => this.get(k)), ); } From 0268ca07e18fdc81487c8c3e22e518d358141b5d Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Thu, 25 Aug 2022 12:30:24 +0200 Subject: [PATCH 13/19] Ensure that questionnaire Kafka topic exists If not, use default questionnaire_response topic. --- src/app/core/services/kafka/kafka.service.ts | 69 +++++++++++++++++-- src/app/core/services/kafka/schema.service.ts | 37 +++++++--- src/app/shared/enums/config.ts | 4 ++ src/app/shared/enums/storage.ts | 4 ++ 4 files changed, 99 insertions(+), 15 deletions(-) diff --git a/src/app/core/services/kafka/kafka.service.ts b/src/app/core/services/kafka/kafka.service.ts index a541a67cb..0f93c3bca 100755 --- a/src/app/core/services/kafka/kafka.service.ts +++ b/src/app/core/services/kafka/kafka.service.ts @@ -15,9 +15,13 @@ import { StorageService } from '../storage/storage.service' import { TokenService } from '../token/token.service' import { AnalyticsService } from '../usage/analytics.service' import { SchemaService } from './schema.service' +import { RemoteConfigService } from "../config/remote-config.service"; +import { ConfigKeys } from "../../../shared/enums/config"; @Injectable() export class KafkaService { + private static DEFAULT_TOPIC_CACHE_VALIDITY = 600_000 // 10 minutes + URI_topics: string = '/topics/' private readonly KAFKA_STORE = { @@ -27,6 +31,9 @@ export class KafkaService { private KAFKA_CLIENT_URL: string private BASE_URI: string private isCacheSending: boolean + private topics: string[] = null + private lastTopicFetch: number = 0 + private TOPIC_CACHE_VALIDITY = KafkaService.DEFAULT_TOPIC_CACHE_VALIDITY constructor( private storage: StorageService, @@ -34,22 +41,71 @@ export class KafkaService { private schema: SchemaService, private analytics: AnalyticsService, private logger: LogService, - private http: HttpClient + private http: HttpClient, + private remoteConfig: RemoteConfigService, ) { this.updateURI() + this.readTopicCacheValidity(); } init() { - return this.setCache({}) + return Promise.all([ + this.setCache({}), + this.updateTopicCacheValidity(), + this.fetchTopics(), + ]); } updateURI() { - this.token.getURI().then(uri => { + return this.token.getURI().then(uri => { this.BASE_URI = uri this.KAFKA_CLIENT_URL = uri + DefaultKafkaURI }) } + readTopicCacheValidity() { + return this.storage.get(StorageKeys.TOPIC_CACHE_TIMEOUT) + .then(timeout => { + if (typeof timeout === 'number') { + this.TOPIC_CACHE_VALIDITY = timeout + } + }); + } + + updateTopicCacheValidity() { + return this.remoteConfig.read() + .then(config => config.getOrDefault(ConfigKeys.TOPIC_CACHE_TIMEOUT, this.TOPIC_CACHE_VALIDITY.toString())) + .then(timeoutString => { + const timeout = parseInt(timeoutString) + if (!isNaN(timeout)) { + this.TOPIC_CACHE_VALIDITY = Math.max(0, timeout) + return this.storage.set(StorageKeys.TOPIC_CACHE_TIMEOUT, this.TOPIC_CACHE_VALIDITY) + } + }) + } + + private fetchTopics() { + return this.http.get(this.KAFKA_CLIENT_URL + this.URI_topics, {observe: 'body'}) + .toPromise() + .then((topics: string[]) => { + this.topics = topics + this.lastTopicFetch = Date.now() + return topics + }) + .catch(e => { + this.logger.error("Failed to fetch Kafka topics", e) + return this.topics + }); + } + + getTopics() { + if (this.topics !== null || this.lastTopicFetch + this.TOPIC_CACHE_VALIDITY >= Date.now()) { + return Promise.resolve(this.topics) + } else { + return this.fetchTopics(); + } + } + prepareKafkaObjectAndSend(type, payload, keepInCache?) { const value = this.schema.getKafkaObjectValue(type, payload) const keyPromise = this.schema.getKafkaObjectKey() @@ -83,14 +139,15 @@ export class KafkaService { return Promise.all([ this.getCache(), this.getKafkaHeaders(), - this.schema.getRadarSpecifications() + this.schema.getRadarSpecifications(), + this.getTopics(), ]) - .then(([cache, headers, specifications]) => { + .then(([cache, headers, specifications, topics]) => { const sendPromises = Object.entries(cache) .filter(([k]) => k) .map(([k, v]: any) => { return this.schema - .getKafkaTopic(specifications, v.name, v.avsc) + .getKafkaTopic(specifications, v.name, v.avsc, topics) .then(topic => this.sendToKafka(topic, k, v, headers)) .catch(e => this.logger.error('Failed to send data from cache to kafka', e) diff --git a/src/app/core/services/kafka/schema.service.ts b/src/app/core/services/kafka/schema.service.ts index b1529ffb8..0e35f95b5 100644 --- a/src/app/core/services/kafka/schema.service.ts +++ b/src/app/core/services/kafka/schema.service.ts @@ -22,11 +22,13 @@ import { QuestionnaireService } from '../config/questionnaire.service' import { RemoteConfigService } from '../config/remote-config.service' import { SubjectConfigService } from '../config/subject-config.service' import { LogService } from '../misc/log.service' +import { valid } from "semver"; @Injectable() export class SchemaService { URI_schema: string = '/schema/subjects/' URI_version: string = '/versions/' + GENERAL_TOPIC: string = 'questionnaire_response' private schemas: { [key: string]: [Promise, Promise] } = {} @@ -153,17 +155,34 @@ export class SchemaService { }) } - getKafkaTopic(specifications: any[] | null, name, avsc): Promise { - try { - const type = name.toLowerCase() - const defaultTopic = `${avsc}_${name}` - if (specifications) { - const spec = specifications.find(t => t.type.toLowerCase() == type) - return Promise.resolve(spec && spec.topic ? spec.topic : defaultTopic) + getKafkaTopic(specifications: any[] | null, name, avsc, topics: string[] | null): Promise { + const type = name.toLowerCase() + + if (specifications) { + const spec = specifications.find(t => t.type.toLowerCase() == type) + if (spec && spec.topic && this.topicExists(spec.topic, topics)) { + return Promise.resolve(spec.topic) } + } + const questionnaireTopic = `${avsc}_${name}` + if (this.topicExists(questionnaireTopic, topics)) { + return Promise.resolve(questionnaireTopic) + } + const defaultTopic = this.GENERAL_TOPIC; + if (this.topicExists(defaultTopic, topics)) { return Promise.resolve(defaultTopic) - } catch (e) { - return Promise.reject('Failed to get kafka topic') + } + + return Promise.reject(`No suitable topic found on server for questionnaire ${name}`) + } + + private topicExists(topic: string, topics: string[] | null) { + if (!topics || topics.includes(topic)) { + return true + } else { + this.logger.error( + `Cannot send data to specification topic ${topic} because target server does not have it`, null) + return false } } diff --git a/src/app/shared/enums/config.ts b/src/app/shared/enums/config.ts index f56e30af2..fcc1326df 100644 --- a/src/app/shared/enums/config.ts +++ b/src/app/shared/enums/config.ts @@ -35,6 +35,10 @@ export class ConfigKeys { static GITHUB_FETCH_STRATEGY = new ConfigKeys('github_fetch_strategy') + static TOPIC_CACHE_TIMEOUT = new ConfigKeys( + 'topic_cache_timeout' + ) + constructor(public value: string) {} toString() { diff --git a/src/app/shared/enums/storage.ts b/src/app/shared/enums/storage.ts index 17d5cdf7c..347ac4c66 100755 --- a/src/app/shared/enums/storage.ts +++ b/src/app/shared/enums/storage.ts @@ -42,6 +42,10 @@ export class StorageKeys { 'REMOTE_CONFIG_CACHE_TIMEOUT' ) + static TOPIC_CACHE_TIMEOUT = new StorageKeys( + 'TOPIC_CACHE_TIMEOUT' + ) + static FCM_TOKEN = new StorageKeys('FCM_TOKEN') static NOTIFICATION_MESSAGING_TYPE = new StorageKeys( 'NOTIFICATION_MESSAGING_TYPE' From 31f9eeb36ca79becb62687d6511227c0373deb7d Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Thu, 25 Aug 2022 13:18:49 +0200 Subject: [PATCH 14/19] Fix tests --- src/app/core/services/kafka/kafka.service.spec.ts | 6 ++++-- src/app/core/services/kafka/schema.service.ts | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/core/services/kafka/kafka.service.spec.ts b/src/app/core/services/kafka/kafka.service.spec.ts index ada9cf541..92a5eb80d 100644 --- a/src/app/core/services/kafka/kafka.service.spec.ts +++ b/src/app/core/services/kafka/kafka.service.spec.ts @@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing' import { FirebaseAnalyticsServiceMock, - LogServiceMock, + LogServiceMock, RemoteConfigServiceMock, SchemaServiceMock, StorageServiceMock, TokenServiceMock @@ -14,6 +14,7 @@ import { TokenService } from '../token/token.service' import { AnalyticsService } from '../usage/analytics.service' import { KafkaService } from './kafka.service' import { SchemaService } from './schema.service' +import { RemoteConfigService } from "../config/remote-config.service"; describe('KafkaService', () => { let service @@ -31,7 +32,8 @@ describe('KafkaService', () => { { provide: AnalyticsService, useClass: FirebaseAnalyticsServiceMock - } + }, + { provide: RemoteConfigService, useClass: RemoteConfigServiceMock }, ] }) ) diff --git a/src/app/core/services/kafka/schema.service.ts b/src/app/core/services/kafka/schema.service.ts index 0e35f95b5..1edc492bc 100644 --- a/src/app/core/services/kafka/schema.service.ts +++ b/src/app/core/services/kafka/schema.service.ts @@ -22,7 +22,6 @@ import { QuestionnaireService } from '../config/questionnaire.service' import { RemoteConfigService } from '../config/remote-config.service' import { SubjectConfigService } from '../config/subject-config.service' import { LogService } from '../misc/log.service' -import { valid } from "semver"; @Injectable() export class SchemaService { From 03e2fde3904fb6205af9e3dadd1d64c6c0559a05 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Tue, 30 Aug 2022 12:21:12 +0100 Subject: [PATCH 15/19] fix: taskDTO mapping and catch appserver schedule error --- src/app/core/services/app-server/app-server.service.ts | 1 + src/app/core/services/schedule/appserver-schedule.service.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/core/services/app-server/app-server.service.ts b/src/app/core/services/app-server/app-server.service.ts index d7ced6e27..4aa52af36 100644 --- a/src/app/core/services/app-server/app-server.service.ts +++ b/src/app/core/services/app-server/app-server.service.ts @@ -274,6 +274,7 @@ export class AppServerService { } ) .toPromise() + .catch(e => []) ) }) } diff --git a/src/app/core/services/schedule/appserver-schedule.service.ts b/src/app/core/services/schedule/appserver-schedule.service.ts index dfbb60dee..968bfcb7c 100644 --- a/src/app/core/services/schedule/appserver-schedule.service.ts +++ b/src/app/core/services/schedule/appserver-schedule.service.ts @@ -82,7 +82,7 @@ export class AppserverScheduleService extends ScheduleService { .getAssessmentForTask(assesmentType, task) .then(assessment => { const newTask = Object.assign(task, { - reportedCompletion: false, + reportedCompletion: !!task.completed, nQuestions: assessment.questions.length, warning: this.localization.chooseText(assessment.warn), requiresInClinicCompletion: assessment.requiresInClinicCompletion, From c490e2d19ad46cc418d8146f4d9d6f53ac2ebf75 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Wed, 31 Aug 2022 01:06:40 +0100 Subject: [PATCH 16/19] fix: tasks service expired task check --- src/app/pages/home/services/tasks.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/home/services/tasks.service.ts b/src/app/pages/home/services/tasks.service.ts index a73ba3917..563d2f6e7 100644 --- a/src/app/pages/home/services/tasks.service.ts +++ b/src/app/pages/home/services/tasks.service.ts @@ -106,7 +106,7 @@ export class TasksService { // NOTE: This checks if completion window has passed or task is complete return ( task.timestamp + task.completionWindow < new Date().getTime() || - (task.completed && !this.wasTaskCompletedToday(task)) + task.completed ) } From cfac87c84e84becd2386efa07a459b178a3a4c3a Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Wed, 31 Aug 2022 01:06:57 +0100 Subject: [PATCH 17/19] fix: notifcation factory hardcoded type --- .../core/services/notifications/notification-factory.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/services/notifications/notification-factory.service.ts b/src/app/core/services/notifications/notification-factory.service.ts index 531006684..8782cfa01 100644 --- a/src/app/core/services/notifications/notification-factory.service.ts +++ b/src/app/core/services/notifications/notification-factory.service.ts @@ -36,7 +36,6 @@ export class NotificationFactoryService extends NotificationService { ) ) .then(type => { - type = NotificationMessagingType.FCM_REST switch (type) { case NotificationMessagingType.LOCAL: return (this.notificationService = this.localNotificationService) From f864dc2e77a9c83a8782c8cf4987a5fbb83d30d1 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Tue, 6 Sep 2022 16:42:58 +0100 Subject: [PATCH 18/19] fix(taskService): not showing completed tasks --- src/app/pages/home/services/tasks.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/pages/home/services/tasks.service.ts b/src/app/pages/home/services/tasks.service.ts index 563d2f6e7..c44535e0a 100644 --- a/src/app/pages/home/services/tasks.service.ts +++ b/src/app/pages/home/services/tasks.service.ts @@ -54,7 +54,11 @@ export class TasksService { getTasksOfToday() { return this.schedule .getTasksForDate(new Date(), AssessmentType.SCHEDULED) - .then(tasks => tasks.filter(t => !this.isTaskExpired(t))) + .then(tasks => + tasks.filter( + t => !this.isTaskExpired(t) || this.wasTaskCompletedToday(t) + ) + ) } getValidTasksMap(): Promise> { From 27479a9802fd215199c0753c52faf763a3e10548 Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Thu, 8 Sep 2022 22:49:46 +0100 Subject: [PATCH 19/19] chore: bump versions --- config.xml | 2 +- src/assets/data/defaultConfig.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.xml b/config.xml index eb723a609..22d52d7ed 100755 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + RADAR Questionnaire An application that collects active data for research. RADAR-Base diff --git a/src/assets/data/defaultConfig.ts b/src/assets/data/defaultConfig.ts index b6d5a03a5..3d7118355 100755 --- a/src/assets/data/defaultConfig.ts +++ b/src/assets/data/defaultConfig.ts @@ -16,7 +16,7 @@ import { Localisations } from './localisations' export const DefaultPlatformInstance = 'RADAR-CNS' // *Default app version -export const DefaultAppVersion = '2.4.0-alpha' +export const DefaultAppVersion = '2.6.0-alpha' // *Default Android package name export const DefaultPackageName = 'org.phidatalab.radar_armt'