From 2a8fb745eaa8206c277e1216ce06d38cd2ca47b5 Mon Sep 17 00:00:00 2001 From: Kven Ho Date: Mon, 2 Oct 2017 12:44:34 +0800 Subject: [PATCH 01/14] Move assessment and submission [ISDK-75] --- src/services/assessment.service.ts | 160 ++++++++++++++++++++++++++--- src/services/submission.service.ts | 3 +- 2 files changed, 149 insertions(+), 14 deletions(-) diff --git a/src/services/assessment.service.ts b/src/services/assessment.service.ts index a8f7900c..859e35dd 100755 --- a/src/services/assessment.service.ts +++ b/src/services/assessment.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; import { RequestService } from '../shared/request/request.service'; - +import { Http, Headers } from '@angular/http'; import * as _ from 'lodash'; - +// services +import { CacheService } from '../shared/cache/cache.service'; class Assessment { id: number; context_id: number; @@ -15,6 +16,15 @@ class Answer { choices?: Array; } +export class questionsResult { + required: Boolean; + answer: any; + reviewerAnswer: { + answer: any; + comment: any; + }; +} + export class ChoiceBase { id: number; value?: number; // or choice id, usually same as "id" above @@ -32,11 +42,12 @@ export class QuestionBase { assessment_id: number; name: string; type: string; - file_type?: string; - audience: Array; + description: string; + required: boolean; + audience: string | Array; + file_type?: string | any; choices?: ChoiceBase[]; answer?: any; - required?: boolean; order?: string | number; constructor(id, assessment_id, name, type) { @@ -55,8 +66,12 @@ export class Submission { @Injectable() export class AssessmentService { - constructor(private request: RequestService) {} - + constructor(private cacheService: CacheService, + private request: RequestService, + private http: Http) {} + private prefixUrl: any = this.request.getPrefixUrl(); + private appkey = this.request.getAppkey(); + private assessment_url = 'api/assessments.json'; /** * @description check feedback can show * @type {boolen} @@ -268,9 +283,10 @@ export class AssessmentService { */ public normaliseGroup(group) { // let result = group; - let thisQuestions = group.AssessmentGroupQuestion; - thisQuestions = thisQuestions.map(question => { - return this.normaliseQuestion(question); + let questions = group.AssessmentGroupQuestion; + let thisQuestions = []; + questions.forEach(question => { + thisQuestions.push(this.normaliseQuestion(question)); }); return { @@ -283,6 +299,27 @@ export class AssessmentService { } } + /** + * filter submission by: + * - "submitter" as audience + * - "submitter" as audience && status as "published" + * @name isAccessible + * @param {object} question Single normalised assessment + * object from this.normalise above + */ + public isAccessible(question, status) { + let result = true; + if (question.audience.indexOf('submitter') === -1) { + result = false; + } + + if (result && status === 'published') { + result = false; + } + + return result; + } + /* turn "AssessmentGroupQuestion" array format from: { @@ -326,18 +363,19 @@ export class AssessmentService { }); return { - id: question.id, + id: question.id, // unknown purpose (be careful with this id) assessment_id: question.assessment_question_id, - question_id: question.assessment_question_id, + question_id: question.assessment_question_id, // use this to indicate question group_id: question.assessment_group_id, name: thisQuestion.name, type: thisQuestion.question_type, audience: thisQuestion.audience, + description: thisQuestion.description, file_type: thisQuestion.file_type, required: thisQuestion.is_required, choices: choices, order: question.order, - answer: thisQuestion.answer + answer: thisQuestion.answer, }; } @@ -379,4 +417,100 @@ export class AssessmentService { weight: choice.weight }; } + + /** + * hardcode communication to different server + * @param {[type]} assessment_id [description] + */ + public getPostProgramAssessment(assessment_id) { + // let url = `${this.prefixUrl}api/assessments.json?assessment_id=${assessment_id}&structured=true`; + // let headers = new Headers(); + // headers.append('appkey', this.appkey); + // headers.append('apikey', this.cacheService.getLocalObject('apikey')); + // headers.append('timelineID', this.cacheService.getLocalObject('timelineID')); + return this.request.get(`api/assessments.json?assessment_id=${assessment_id}&structured=true`); + } + + // helpers + public getStatus(questionsResult, submissionResult): string { + let questionsStatus = []; + _.forEach(questionsResult, q => { + if (q.required && q.answer !== null) { + if ( + q.reviewerAnswer !== null && + submissionResult.status !== 'pending approval' && + (q.reviewerAnswer.answer || q.reviewerAnswer.comment) + ) { + questionsStatus.push('reviewed'); + } else { + questionsStatus.push('completed'); + } + } + + if (!q.required && q.answer !== null) { + if ( + q.reviewerAnswer !== null && + submissionResult.status !== 'pending approval' && + (q.reviewerAnswer.answer || q.reviewerAnswer.comment) + ) { + questionsStatus.push('reviewed'); + } else { + questionsStatus.push('completed'); + } + } + + if (q.answer === null) { + questionsStatus.push('incomplete'); + } + }); + + // get final status by checking all questions' statuses + let status = 'incomplete'; + if (_.every(questionsStatus, (v) => { + return (v === 'completed'); + })) { + status = 'completed'; + } + if (_.includes(questionsStatus, 'reviewed')) { + status = 'reviewed'; + } + + return status; + } + + public getSummaries(questionsResult: Array) { + let totalRequiredQuestions = 0; + let answeredQuestions = 0; + let reviewerFeedback = 0; + + _.forEach(questionsResult, (q) => { + // get total number of questions + if (q.required) { + totalRequiredQuestions += 1; + } + + // get total number of answered questions + if (q.required && q.answer && q.answer !== null) { + answeredQuestions += 1; + } + + // get total number of feedback + // If API response, the reviewer's answer and comment are empty, + // front-end don't consider it as a feedback + if ( + q.reviewerAnswer && + q.reviewerAnswer !== null && + !_.isEmpty(q.reviewerAnswer.answer) && + !_.isEmpty(q.reviewerAnswer.comment) + ) { + reviewerFeedback += 1; + } + }); + + return { + totalRequiredQuestions, + answeredQuestions, + reviewerFeedback + }; + } } diff --git a/src/services/submission.service.ts b/src/services/submission.service.ts index 9371f079..47574abb 100755 --- a/src/services/submission.service.ts +++ b/src/services/submission.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { RequestService } from '../shared/request/request.service'; +import { Observable } from 'rxjs/Observable'; import * as _ from 'lodash'; import * as moment from 'moment'; @@ -211,7 +212,7 @@ export class SubmissionService { * extract reference IDs and prepare Observables to retrieve submissions * @param {array} references References array responded with get_activities() api */ - public getSubmissionsByReferences(references) { + public getSubmissionsByReferences(references: Array<{context_id : Number}>): Array> { let tasks = []; // multiple API requests // get_submissions API to retrieve submitted answer From e4a11c033d220d8b2846e35dc800e211915f1b96 Mon Sep 17 00:00:00 2001 From: Kven Ho Date: Mon, 2 Oct 2017 16:53:48 +0800 Subject: [PATCH 02/14] Fix render error problem [ISDK-75] --- .../assessments/group/assessments-group.html | 21 +- .../group/assessments-group.page.ts | 227 ++++++++---------- 2 files changed, 108 insertions(+), 140 deletions(-) diff --git a/src/pages/assessments/group/assessments-group.html b/src/pages/assessments/group/assessments-group.html index 43f8bcd9..dc0b3ad5 100644 --- a/src/pages/assessments/group/assessments-group.html +++ b/src/pages/assessments/group/assessments-group.html @@ -16,12 +16,13 @@ - +
{{assessmentGroup.name}}
-

+
@@ -30,43 +31,39 @@
{{assessmentGroup.name}}
* - +

-
-
{{ submission | json }}
-
- - +
diff --git a/src/pages/assessments/group/assessments-group.page.ts b/src/pages/assessments/group/assessments-group.page.ts index 7f434f9f..d868be9d 100644 --- a/src/pages/assessments/group/assessments-group.page.ts +++ b/src/pages/assessments/group/assessments-group.page.ts @@ -1,16 +1,15 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { NavParams, NavController, AlertController, LoadingController, Events } from 'ionic-angular'; import { FormBuilder, Validators, FormGroup, FormControl, FormArray } from '@angular/forms'; import { CacheService } from '../../../shared/cache/cache.service'; import { ChoiceBase, QuestionBase, Submission, AssessmentService } from '../../../services/assessment.service'; import * as _ from 'lodash'; - @Component({ selector: 'assessments-group-page', templateUrl: './assessments-group.html', }) -export class AssessmentsGroupPage { +export class AssessmentsGroupPage implements OnInit { questions = []; formGroup; @@ -40,7 +39,7 @@ export class AssessmentsGroupPage { public events: Events ) {} - ionViewDidEnter() { + ngOnInit() { // navigate from activity page this.activity = this.navParams.get('activity') || {}; @@ -49,15 +48,15 @@ export class AssessmentsGroupPage { if (!_.isEmpty(this.event)) { this.activity = this.event; } + } - this.assessment = this.activity.assessment; // required for context_id - this.cacheKey = `assessment.group.${this.assessment.context_id}`; + ionViewDidEnter() { + // use assessment object from activity (required for extracting context_id) + this.assessment = this.activity.assessment; + this.cacheKey = `assessment.group.${this.assessment.context_id}`; this.assessmentGroup = this.navParams.get('assessmentGroup') || {}; this.submission = this.navParams.get('submission') || {}; - - console.log('this.assessmentGroup', this.assessmentGroup); - // preset key used for caching later (locally and remote data) this.canUpdateInput = this.isInputEditable(this.submission); // this.published = this.assessmentService.isPublished(this.submissions); @@ -69,6 +68,10 @@ export class AssessmentsGroupPage { ); } + /** + * @name updateSubmission + * @description trace changes of input for assessment (to avoid extra checking logics) + */ updateSubmission() { this.events.publish('assessment:changes', { changed: true @@ -86,22 +89,6 @@ export class AssessmentsGroupPage { return true; } return false; - // let editable = false; - // _.forEach(this.submissions, (submission) => { - // if (_.isEmpty(submission)) { - // editable = true; - // } else { - // _.forEach(submission, (subm) => { - // if ( - // subm.AssessmentSubmission && - // subm.AssessmentSubmission.status === 'in progress' - // ) { - // editable = true; - // } - // }); - // } - // }); - // return editable; } /** @@ -116,24 +103,32 @@ export class AssessmentsGroupPage { } _.forEach(submission.review, (review) => { + _.forEach(questions, (question, idx) => { - if (review.assessment_question_id === question.id) { - // text type + if (review.assessment_question_id === question.question_id) { + // text type (no merging, text question displayed in plain text) if (question.type === 'text') { questions[idx].review_answer = review; } // oneof type + // combine question, when answered by both reviewer and submitter if (question.type === 'oneof') { questions[idx].review_answer = review; + let submitterAnswer = question.answer; + _.forEach(question.choices, (choice, key) => { - if (choice.id == review.answer && choice.id == question.answer.answer) { - questions[idx].choices[key].name = choice.name + ' (you and reviewer)'; - } - if (choice.id != review.answer && choice.id == question.answer.answer) { - questions[idx].choices[key].name = choice.name + ' (you)'; - } - if (choice.id == review.answer && choice.id != question.answer.answer) { + if (!_.isEmpty(submitterAnswer)) { + if (choice.id == review.answer && choice.id == submitterAnswer.answer) { + questions[idx].choices[key].name = choice.name + ' (you and reviewer)'; + } + else if (choice.id != review.answer && choice.id == submitterAnswer.answer) { + questions[idx].choices[key].name = choice.name + ' (you)'; + } + else if (choice.id == review.answer && choice.id != submitterAnswer.answer) { + questions[idx].choices[key].name = choice.name + ' (reviewer)'; + } + } else if (choice.id == review.answer) { // display reviewer answer questions[idx].choices[key].name = choice.name + ' (reviewer)'; } }); @@ -142,39 +137,6 @@ export class AssessmentsGroupPage { }); }); - // _.forEach(submissions, (submission) => { - // _.forEach(submission, (subm) => { - // - // _.forEach(subm.AssessmentReviewAnswer, (reviewAnswer) => { - // _.forEach(questions, (question, idx) => { - // - // if (reviewAnswer.assessment_question_id === question.id) { - // // text type - // if (question.type === 'text') { - // questions[idx].review_answer = reviewAnswer; - // } - // - // // oneof type - // if (question.type === 'oneof') { - // questions[idx].review_answer = reviewAnswer; - // _.forEach(question.choices, (choice, key) => { - // if (choice.id == reviewAnswer.answer && choice.id == question.answer.answer) { - // questions[idx].choices[key].name = choice.name + ' (you and reviewer)'; - // } - // if (choice.id != reviewAnswer.answer && choice.id == question.answer.answer) { - // questions[idx].choices[key].name = choice.name + ' (you)'; - // } - // if (choice.id == reviewAnswer.answer && choice.id != question.answer.answer) { - // questions[idx].choices[key].name = choice.name + ' (reviewer)'; - // } - // }); - // } - // } - // - // }); - // }); - // }); - // }); return questions; } @@ -236,7 +198,14 @@ export class AssessmentsGroupPage { group['choices'] = new FormGroup(choices); } - result[question.id] = new FormGroup(group); + /** + * id and question_id are different id + * - id = has no obvious purpose + * - question_id must be used as id for submission + * + * but for case like this just for index id + */ + result[question.question_id] = new FormGroup(group); }); return result; @@ -262,22 +231,31 @@ export class AssessmentsGroupPage { return { Assessment: { - id: submission.assessment_id, - context_id: this.getSubmissionContext() + id: submission.assessment_id, + context_id: this.getSubmissionContext() }, AssessmentSubmissionAnswer: answers }; } /** - * @description store assessment answer/progress locally + * @name storeProgress + * @description store assessment answer/progress locally (offline) + * @example format for cached submission + * { + * Assessment: { + * id: 1, + * context_id: 2 + * }, + * AssessmentSubmissionAnswer: Array + * } */ storeProgress = () => { let answers = {}; - _.forEach(this.formGroup, (question, id) => { + _.forEach(this.formGroup, (question, question_id) => { let values = question.getRawValue(), answer = { - assessment_question_id: id, + assessment_question_id: question_id, answer: values.answer || values.comment, // store it if choice answer is available or skip @@ -286,7 +264,7 @@ export class AssessmentsGroupPage { // set empty string to remove answer answer.answer = (answer.answer) ? answer.answer : ''; - answers[id] = answer; + answers[question_id] = answer; }); // final step - store submission locally @@ -308,18 +286,17 @@ export class AssessmentsGroupPage { * @description retrieve saved progress from localStorage */ retrieveProgress = (questions: Array, answers?) => { - let cachedProgress = answers || {}; //this.cache.getLocalObject(this.cacheKey); - - let newQuestions = questions; - let savedProgress = cachedProgress.AssessmentSubmissionAnswer; + let cachedProgress = answers || {}, + newQuestions = questions, + savedProgress = cachedProgress.AssessmentSubmissionAnswer; if (!_.isEmpty(savedProgress)) { - // index "id" is set as question.id in @Function buildFormGroup above - _.forEach(newQuestions, (question, id) => { + // index "id" is set as question.question_id in @Function buildFormGroup above + _.forEach(newQuestions, (question, question_id) => { // check integrity of saved answer (question might get updated) - if (savedProgress[id] && savedProgress[id].assessment_question_id == id) { - newQuestions[id] = this.setValueWith(question, savedProgress[id]); + if (savedProgress[question_id] && savedProgress[question_id].assessment_question_id == question_id) { + newQuestions[question_id] = this.setValueWith(question, savedProgress[question_id]); } }); } @@ -347,58 +324,52 @@ export class AssessmentsGroupPage { } /** - * @description initiate save progress and return to previous page/navigation stack + * @name save + * @description save input (partially post submission) and + * return to previous navigation stack */ save() { let self = this, - loading = this.loadingCtrl.create({ - content: 'Loading...' - }), - // to provide a more descriptive error message (if available) - failAlert = this.alertCtrl.create({ - title: 'Fail to submit.' - }); - - let saveProgress = () => { - this.updateSubmission(); - - loading.present().then(() => { - self.assessmentService.save(self.storeProgress()).subscribe( - response => { - loading.dismiss().then(() => { - self.navCtrl.pop(); - }); - }, - reject => { - loading.dismiss().then(() => { - failAlert.data.title = reject.msg || failAlert.data.title; - failAlert.present().then(() => { - console.log('Unable to save', reject); + loading = this.loadingCtrl.create({ + content: 'Loading...' + }), + // to provide a more descriptive error message (if available) + failAlert = this.alertCtrl.create({ + title: 'Fail to submit.' + }), + saveProgress = () => { + this.updateSubmission(); + + loading.present().then(() => { + self.assessmentService.save(self.storeProgress()).subscribe( + response => { + loading.dismiss().then(() => { + self.navCtrl.pop(); }); - }); - } - ); + }, + reject => { + loading.dismiss().then(() => { + failAlert.data.title = reject.msg || failAlert.data.title; + failAlert.present().then(() => { + console.log('Unable to save', reject); + }); + }); + } + ); + }); + }, + confirmBox = this.alertCtrl.create({ + message: 'You have not completed all required questions. Do you still wish to Save?', + buttons: [ + { + text: 'Yes', + handler: saveProgress + }, + 'No' + ] }); - }; - - let confirmBox = this.alertCtrl.create({ - message: 'You have not completed all required questions. Do you still wish to Save?', - buttons: [ - { - text: 'Yes', - handler: () => { - saveProgress(); - } - }, - { - text: 'No', - handler: () => { - //return false; - } - } - ] - }); + // has all compulsory questions answered? if (!this.isAllQuestionsAnswered()) { confirmBox.present(); } else { From 4d80266904c7f6acb897dd148e0ab33b457d3609 Mon Sep 17 00:00:00 2001 From: Kven Ho Date: Mon, 2 Oct 2017 17:04:59 +0800 Subject: [PATCH 03/14] Fix problem unable to submit [ISDK-75] --- src/pages/assessments/assessments.html | 10 +- src/pages/assessments/assessments.page.ts | 503 ++++++++++------------ 2 files changed, 234 insertions(+), 279 deletions(-) diff --git a/src/pages/assessments/assessments.html b/src/pages/assessments/assessments.html index 02d56f6b..bb0fe09f 100644 --- a/src/pages/assessments/assessments.html +++ b/src/pages/assessments/assessments.html @@ -1,7 +1,6 @@ -