From b508362262b80e42cbe083760a6f9356a4ddc90f Mon Sep 17 00:00:00 2001 From: Kven Ho Date: Thu, 16 Nov 2017 10:31:49 +0800 Subject: [PATCH 1/4] Fix render incorrect assessment [ISDK-75] --- src/pages/assessments/assessments.page.ts | 51 +++--- src/pages/events/view/events-view.page.ts | 3 +- src/services/assessment.service.ts | 194 +++++++++++++++++++--- src/services/submission.service.ts | 24 +-- 4 files changed, 212 insertions(+), 60 deletions(-) diff --git a/src/pages/assessments/assessments.page.ts b/src/pages/assessments/assessments.page.ts index bb5ba3a8..24694ecd 100644 --- a/src/pages/assessments/assessments.page.ts +++ b/src/pages/assessments/assessments.page.ts @@ -32,6 +32,7 @@ export class AssessmentsPage { assessmentQuestions: any = []; combinedItems: any = []; discardConfirmMessage = confirmMessages.Assessments.DiscardChanges.discard; + event: any = {}; getCharacterID: any = this.cacheService.getLocal('character_id'); getInitialItems: any = this.cacheService.getLocal('initialItems'); gotNewItems: boolean = false; @@ -61,6 +62,7 @@ export class AssessmentsPage { public translationService: TranslationService ) { this.activity = this.navParams.get('activity'); + this.event = this.navParams.get('event'); if (!this.activity) { throw 'Fatal Error: Activity not available'; } @@ -282,26 +284,6 @@ export class AssessmentsPage { loadQuestions(): Promise { return new Promise((resolve, reject) => { - - // get_assessments request with 'assessment_id' & 'structured' - let getAssessment = (assessmentId) => { - return this.assessmentService.getAll({ - assessment_id: assessmentId, - structured: true - }); - }; - - // Congregation of assessment ids to fulfill get_assessments API's param requirement - let tasks = []; - _.forEach(this.activity.References, (reference) => { - if ( - reference.Assessment && - reference.Assessment.id - ) { - return tasks.push(getAssessment(reference.Assessment.id)); - } - }); - /** * merging submission into question inside of assessment array objects * - set question statuses (quantity of total answered) @@ -349,7 +331,7 @@ export class AssessmentsPage { }; // first batch API requests (get_assessments) - Observable.forkJoin(tasks) + Observable.forkJoin(this.preStackTasks()) .subscribe( (assessments: any) => { this.assessmentGroups = assessments; @@ -381,6 +363,33 @@ export class AssessmentsPage { }); } + /** + * @name preStackTasks + * @description stack of tasks prepared to handle multiple activity references (ids) + */ + preStackTasks() { + // get_assessments request with "assessment_id" & "structured" + let getAssessment = (assessmentId) => { + // @TODO: we might need to pass in submission id (if available) to get properly filtered assessmnet questions + return this.assessmentService.getAll({ + search: { + assessment_id: assessmentId, + structured: true + } + }); + }; + + let tasks: Array = []; + // Congregate assessment ids for rxjs forkJoin (batch API requests) + _.forEach(this.event.References, ref => { + if (ref.Assessment && ref.Assessment.id) { + tasks.push(getAssessment(ref.Assessment.id)); + } + }); + + return tasks; + } + /** * submit answer and change submission status to done diff --git a/src/pages/events/view/events-view.page.ts b/src/pages/events/view/events-view.page.ts index 9d8c3899..ed408924 100755 --- a/src/pages/events/view/events-view.page.ts +++ b/src/pages/events/view/events-view.page.ts @@ -201,9 +201,8 @@ export class EventsViewPage { loading.present().then(() => { // if submission exist loading.dismiss().then(() => { - // this.navCtrl.push(AssessmentsGroupPage, { this.navCtrl.push(AssessmentsPage, { - event, + event: event, activity: event.activity, submissions: this.submissions }); diff --git a/src/services/assessment.service.ts b/src/services/assessment.service.ts index dbc0e492..cb2b5b55 100755 --- a/src/services/assessment.service.ts +++ b/src/services/assessment.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { RequestService } from '../shared/request/request.service'; - import * as _ from 'lodash'; - +// services +import { CacheService } from '../shared/cache/cache.service'; class Assessment { id: number; context_id: number; @@ -15,6 +15,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 +41,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,7 +65,12 @@ export class Submission { @Injectable() export class AssessmentService { + assessmentsUrl = 'api/assessments.json'; + exportAssessmentsUrl = 'api/export_assessments.json'; + assessmentSubmissionsUrl = 'api/assessment_submissions.json'; + constructor( + public cacheService: CacheService, public request: RequestService ) {} @@ -78,9 +93,12 @@ export class AssessmentService { return published; } - // listAll() - getAll(options?: any) { - return this.request.get('api/assessments.json', { search: options }); + /** + * @description Get all Assessments + * @param {any} options + */ + getAll(options?: object) { + return this.request.get(this.assessmentsUrl, options); } /** @@ -91,33 +109,36 @@ export class AssessmentService { * - checkbox values (selected checkbox ids are required for post_assessments API) which * they are only available in this get_export_assessments api * - * @param {any} options [description] + * @param {any} options */ - getQuestion(options?: any) { - return this.request.get('api/export_assessments.json', { search: options }); + getQuestion(options?: object) { + return this.request.get(this.exportAssessmentsUrl, options); } + /** + * @description Submit answer for an assessment + * @param {Submission} user's answer + */ post(assessmentAnswer: Submission) { - return this.request.post('api/assessment_submissions.json', assessmentAnswer, { + return this.request.post(this.assessmentSubmissionsUrl, assessmentAnswer, { 'Content-Type': 'application/json' }); } /** - * save progress using "post" function AssessmentService.post() - * @param {Object} assessmentAnswer + * @description Save progress using "post" function AssessmentService.post() + * @param {Submission} assessmentAnswer */ - save(assessmentAnswer) { - assessmentAnswer.Assessment.in_progress = true; // force in_progress - + save(assessmentAnswer: Submission) { + assessmentAnswer.Assessment.in_progress = true; // force to save as in progress return this.post(assessmentAnswer); } /** * submit using "post" function AssessmentService.post() - * @param {Object} assessmentAnswer + * @param {Submission} assessmentAnswer */ - submit(assessmentAnswer) { + submit(assessmentAnswer: Submission) { return this.post(assessmentAnswer); } @@ -269,10 +290,10 @@ export class AssessmentService { } */ 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 { @@ -285,6 +306,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 + * @param {string} status + */ + 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: { @@ -319,7 +361,7 @@ export class AssessmentService { "order": null, } */ - normaliseQuestion(question): QuestionBase { + normaliseQuestion(question): QuestionBase { let thisQuestion = question.AssessmentQuestion; let choices = thisQuestion.AssessmentQuestionChoice; @@ -328,18 +370,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, }; } @@ -381,4 +424,101 @@ export class AssessmentService { weight: choice.weight }; } + + /** + * hardcode communication to different server + * @param {[type]} assessment_id [description] + */ + getPostProgramAssessment(assessment_id) { + return this.request.get(`${this.assessmentsUrl}?assessment_id=${assessment_id}&structured=true`); + } + + /** + * @description get submission status + */ + 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'); + } + + if(q.answer === null && q.audience == '["reviewer"]'){ + questionsStatus.push('reviewed'); + } + }); + + // 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; + } + + 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 562de8dc..704ebf3c 100755 --- a/src/services/submission.service.ts +++ b/src/services/submission.service.ts @@ -1,21 +1,21 @@ import { Injectable } from '@angular/core'; - -// Others import { RequestService } from '../shared/request/request.service'; +import { Observable } from 'rxjs/Observable'; + import * as _ from 'lodash'; import * as moment from 'moment'; @Injectable() export class SubmissionService { - targetUrl = 'api/submissions.json'; + private targetUrl = 'api/submissions.json'; - constructor( - public request: RequestService - ) {} + constructor(private request: RequestService) {} - // list() + /** + * @description List all submissions + */ getSubmissions(options?: any) { - return this.request.get(this.targetUrl, { search: options }); + return this.request.get(this.targetUrl, options); } extractPhotos(data) { @@ -214,12 +214,16 @@ export class SubmissionService { * extract reference IDs and prepare Observables to retrieve submissions * @param {array} references References array responded with get_activities() api */ - getSubmissionsByReferences(references) { + getSubmissionsByReferences(references: Array<{context_id : Number}>): Array> { let tasks = []; // multiple API requests // get_submissions API to retrieve submitted answer let getSubmissions = (contextId) => { - return this.getSubmissions({ context_id: contextId }); + return this.getSubmissions({ + search: { + context_id: contextId + } + }); }; // Congregation of get_submissions API Observable with different context_id _.forEach(references, reference => { From 7a4292dc4b11f1c6e256128c0454fafee4c198d3 Mon Sep 17 00:00:00 2001 From: Kven Ho Date: Mon, 2 Oct 2017 16:53:48 +0800 Subject: [PATCH 2/4] Fix render error problem [ISDK-75] --- .../assessments/group/assessments-group.html | 21 +-- .../group/assessments-group.page.ts | 177 ++++++++++-------- 2 files changed, 109 insertions(+), 89 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 568e4cf2..3da1f7fa 100644 --- a/src/pages/assessments/group/assessments-group.page.ts +++ b/src/pages/assessments/group/assessments-group.page.ts @@ -1,4 +1,4 @@ -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'; @@ -6,12 +6,11 @@ import { FormBuilder, Validators, FormGroup, FormControl, FormArray } from '@ang 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 { activity: any; answers: any; // to render & display submitted answers assessment: any; @@ -36,7 +35,7 @@ export class AssessmentsGroupPage { public navParams: NavParams ) {} - ionViewDidEnter() { + ngOnInit() { // navigate from activity page this.activity = this.navParams.get('activity') || {}; @@ -45,13 +44,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') || {}; - // preset key used for caching later (locally and remote data) this.canUpdateInput = this.isInputEditable(this.submission); // this.published = this.assessmentService.isPublished(this.submissions); @@ -63,6 +64,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 @@ -92,24 +97,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)'; } }); @@ -117,6 +130,7 @@ export class AssessmentsGroupPage { } }); }); + return questions; } @@ -178,7 +192,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; @@ -204,22 +225,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 @@ -228,7 +258,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 @@ -250,18 +280,17 @@ export class AssessmentsGroupPage { * @description retrieve saved progress from localStorage */ retrieveProgress = (questions: Array, answers?) => { - let cachedProgress = answers || {}; //this.cache.getLocal(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]); } }); } @@ -289,58 +318,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 5049473f250cdb9871d57481b3edd0ffd0587d8f Mon Sep 17 00:00:00 2001 From: Kven Ho Date: Mon, 2 Oct 2017 17:04:59 +0800 Subject: [PATCH 3/4] Fix problem unable to submit [ISDK-75] --- src/pages/assessments/assessments.html | 10 +- src/pages/assessments/assessments.page.ts | 485 +++++++++++----------- 2 files changed, 239 insertions(+), 256 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 @@ -