MOBILE-2339 feedback: Implement form page
This commit is contained in:
		
							parent
							
								
									fca428843e
								
							
						
					
					
						commit
						d8d34a786a
					
				| @ -338,11 +338,11 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity | ||||
|      * | ||||
|      * @param {boolean} preview Preview or edit the form. | ||||
|      */ | ||||
|     gotoAnswerQuestions(preview: boolean): void { | ||||
|     gotoAnswerQuestions(preview: boolean = false): void { | ||||
|         const stateParams = { | ||||
|             module: this.module, | ||||
|             moduleid: this.module.id, | ||||
|             courseid: this.courseId, | ||||
|             moduleId: this.module.id, | ||||
|             courseId: this.courseId, | ||||
|             preview: preview | ||||
|         }; | ||||
|         this.navCtrl.push('AddonModFeedbackFormPage', stateParams); | ||||
|  | ||||
| @ -3,24 +3,32 @@ | ||||
|     "anonymous": "Anonymous", | ||||
|     "anonymous_entries": "Anonymous entries ({{$a}})", | ||||
|     "average": "Average", | ||||
|     "captchaofflinewarning": "Feedback with CAPTCHA cannot be completed offline, or if not configured, or if the server is down.", | ||||
|     "completed_feedbacks": "Submitted answers", | ||||
|     "complete_the_form": "Answer the questions...", | ||||
|     "continue_the_form": "Continue the form", | ||||
|     "feedbackclose": "Allow answers to", | ||||
|     "feedbackopen": "Allow answers from", | ||||
|     "feedback_is_not_open": "The feedback is not open", | ||||
|     "feedback_submitted_offline": "This feedback has been saved to be submitted later.", | ||||
|     "mapcourses": "Map feedback to courses", | ||||
|     "mode": "Mode", | ||||
|     "next_page": "Next page", | ||||
|     "non_anonymous": "User's name will be logged and shown with answers", | ||||
|     "non_anonymous_entries": "Non anonymous entries ({{$a}})", | ||||
|     "non_respondents_students": "Non respondents students ({{$a}})", | ||||
|     "not_selected": "Not selected", | ||||
|     "not_started": "Not started", | ||||
|     "numberoutofrange": "Number out of range", | ||||
|     "overview": "Overview", | ||||
|     "page_after_submit": "Completion message", | ||||
|     "preview": "Preview", | ||||
|     "previous_page": "Previous page", | ||||
|     "questions": "Questions", | ||||
|     "responses": "Responses", | ||||
|     "response_nr": "Response number", | ||||
|     "save_entries": "Submit your answers", | ||||
|     "show_entries": "Show responses", | ||||
|     "show_nonrespondents": "Show non-respondents", | ||||
|     "started": "Started", | ||||
|     "this_feedback_is_already_submitted": "You've already completed this activity." | ||||
|  | ||||
							
								
								
									
										116
									
								
								src/addon/mod/feedback/pages/form/form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/addon/mod/feedback/pages/form/form.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar> | ||||
|         <ion-title><core-format-text  [text]=" title "></core-format-text></ion-title> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-loading [hideUntil]="feedbackLoaded"> | ||||
|         <ng-container *ngIf="items && items.length"> | ||||
|             <ion-list no-margin> | ||||
|                 <ion-item text-wrap> | ||||
|                     <h2>{{ 'addon.mod_feedback.mode' | translate }}</h2> | ||||
|                     <p *ngIf="access.isanonymous">{{ 'addon.mod_feedback.anonymous' | translate }}</p> | ||||
|                     <p *ngIf="!access.isanonymous">{{ 'addon.mod_feedback.non_anonymous' | translate }}</p> | ||||
|                 </ion-item> | ||||
|                 <ng-container *ngFor="let item of items"> | ||||
|                     <ion-item-divider *ngIf="item.typ == 'pagebreak'" color="light"></ion-item-divider> | ||||
|                     <ion-item text-wrap *ngIf="item.typ != 'pagebreak'" [color]="item.dependitem > 0 ? 'light' : ''" [class.core-danger-item]="item.isEmpty || item.hasError"> | ||||
|                         <ion-label *ngIf="item.name" [core-mark-required]="item.required" stacked> | ||||
|                             <span *ngIf="item.itemnumber">{{item.itemnumber}}. </span>{{ item.name }} | ||||
|                         </ion-label> | ||||
|                         <div item-content class="addon-mod_feedback-form-content" *ngIf="item.template"> | ||||
|                             <ng-container [ngSwitch]="item.template"> | ||||
|                                 <ng-container *ngSwitchCase="'label'"> | ||||
|                                     <p><core-format-text [component]="component" [componentId]="componentId" [text]="item.presentation"></core-format-text></p> | ||||
|                                 </ng-container> | ||||
|                                 <ng-container *ngSwitchCase="'textfield'"> | ||||
|                                     <ion-input type="text" [(ngModel)]="item.value" autocorrect="off" name="{{item.typ}}_{{item.id}}" maxlength="{{item.maxlength}}" [required]="item.required"></ion-input> | ||||
|                                 </ng-container> | ||||
|                                 <ng-container *ngSwitchCase="'numeric'"> | ||||
|                                     <ion-input [required]="item.required" name="{{item.typ}}_{{item.id}}" type="number" [(ngModel)]="item.value"></ion-input> | ||||
|                                     <p *ngIf="item.hasError" color="error">{{ 'addon.mod_feedback.numberoutofrange' |translate }} [{{item.rangefrom}}<span *ngIf="item.rangefrom && item.rangeto">, </span>{{item.rangeto}}]</p> | ||||
|                                 </ng-container> | ||||
|                                 <ng-container *ngSwitchCase="'textarea'"> | ||||
|                                     <ion-textarea [required]="item.required" name="{{item.typ}}_{{item.id}}" [attr.aria-multiline]="true" [(ngModel)]="item.value"></ion-textarea> | ||||
|                                 </ng-container> | ||||
|                                 <ng-container *ngSwitchCase="'multichoice-r'"> | ||||
|                                     <ion-list radio-group [(ngModel)]="item.value" [required]="item.required" name="{{item.typ}}_{{item.id}}"> | ||||
|                                         <ion-item *ngFor="let option of item.choices"> | ||||
|                                             <ion-label>{{option.label}}</ion-label> | ||||
|                                             <ion-radio [value]="option.value"></ion-radio> | ||||
|                                         </ion-item> | ||||
|                                     </ion-list> | ||||
|                                 </ng-container> | ||||
|                                 <ion-list *ngSwitchCase="'multichoice-c'"> | ||||
|                                     <ion-item *ngFor="let option of item.choices"> | ||||
|                                         <ion-label>{{option.label}}</ion-label> | ||||
|                                         <ion-checkbox [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="option.checked" value="option.value"></ion-checkbox> | ||||
|                                     </ion-item> | ||||
|                                 </ion-list> | ||||
|                                 <ng-container *ngSwitchCase="'multichoice-d'"> | ||||
|                                     <ion-select [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value"> | ||||
|                                         <ion-option *ngFor="let option of item.choices" [value]="option.value">{{option.label}}</ion-option> | ||||
|                                     </ion-select> | ||||
|                                 </ng-container> | ||||
|                                 <ng-container *ngSwitchCase="'captcha'"> | ||||
|                                     <core-recaptcha *ngIf="!preview && !offline" [publicKey]="item.captcha.recaptchapublickey" [model]="item" modelValueName="value"></core-recaptcha> | ||||
|                                     <div *ngIf="!preview && (!item.captcha || offline)" class="core-warning-card" icon-start> | ||||
|                                         <ion-icon name="warning"></ion-icon> | ||||
|                                         {{ 'addon.mod_feedback.captchaofflinewarning' | translate }} | ||||
|                                     </div> | ||||
|                                 </ng-container> | ||||
|                             </ng-container> | ||||
|                         </div> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
|                 <ion-grid> | ||||
|                     <ion-row align-items-center> | ||||
|                         <ion-col *ngIf="hasPrevPage"> | ||||
|                             <button ion-button block outline icon-start (click)="gotoPage(true)"> | ||||
|                                 <ion-icon name="arrow-back"></ion-icon> | ||||
|                                 {{ 'addon.mod_feedback.previous_page' | translate }} | ||||
|                             </button> | ||||
|                         </ion-col> | ||||
|                         <ion-col *ngIf="hasNextPage"> | ||||
|                             <button ion-button block icon-end (click)="gotoPage(false)"> | ||||
|                                 {{ 'addon.mod_feedback.next_page' | translate }} | ||||
|                                 <ion-icon name="arrow-forward"></ion-icon> | ||||
|                             </button> | ||||
|                         </ion-col> | ||||
|                         <ion-col *ngIf="!hasNextPage"> | ||||
|                             <button ion-button block (click)="gotoPage(false)"> | ||||
|                                 {{ 'addon.mod_feedback.save_entries' | translate }} | ||||
|                             </button> | ||||
|                         </ion-col> | ||||
|                     </ion-row> | ||||
|                 </ion-grid> | ||||
|             </ion-list> | ||||
|         </ng-container> | ||||
| 
 | ||||
|         <div class="core-success-card" icon-start *ngIf="completed"> | ||||
|             <ion-icon name="checkmark"></ion-icon> | ||||
|             <p *ngIf="!completionPageContents && !completedOffline">{{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }}</p> | ||||
|             <p *ngIf="!completionPageContents && completedOffline">{{ 'addon.mod_feedback.feedback_submitted_offline' | translate }}</p> | ||||
|             <p *ngIf="completionPageContents"><core-format-text  [component]="component" componentId="componentId" [text]="completionPageContents"></core-format-text></p> | ||||
|         </div> | ||||
| 
 | ||||
|         <ion-grid *ngIf="completed"> | ||||
|             <ion-row align-items-center> | ||||
|                 <ion-col *ngIf="access.canviewanalysis"> | ||||
|                     <button ion-button block outline icon-start (click)="showAnalysis()"> | ||||
|                         <ion-icon name="stats"></ion-icon> | ||||
|                         {{ 'addon.mod_feedback.completed_feedbacks' | translate }} | ||||
|                     </button> | ||||
|                 </ion-col> | ||||
|                 <ion-col *ngIf="hasNextPage"> | ||||
|                     <button ion-button block icon-end (click)="continue()"> | ||||
|                         {{ 'core.continue' | translate }} | ||||
|                         <ion-icon name="arrow-forward"></ion-icon> | ||||
|                     </button> | ||||
|                 </ion-col> | ||||
|             </ion-row> | ||||
|         </ion-grid> | ||||
| 
 | ||||
| 
 | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										37
									
								
								src/addon/mod/feedback/pages/form/form.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/addon/mod/feedback/pages/form/form.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { IonicPageModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CorePipesModule } from '@pipes/pipes.module'; | ||||
| import { AddonModFeedbackComponentsModule } from '../../components/components.module'; | ||||
| import { AddonModFeedbackFormPage } from './form'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModFeedbackFormPage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreDirectivesModule, | ||||
|         CoreComponentsModule, | ||||
|         CorePipesModule, | ||||
|         AddonModFeedbackComponentsModule, | ||||
|         IonicPageModule.forChild(AddonModFeedbackFormPage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class AddonModFeedbackFormPageModule {} | ||||
							
								
								
									
										15
									
								
								src/addon/mod/feedback/pages/form/form.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/addon/mod/feedback/pages/form/form.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| page-addon-mod-feedback-form { | ||||
|     .addon-mod_feedback-form-content { | ||||
|         align-self: self-start; | ||||
|         width: 100%; | ||||
|     } | ||||
|     .item-md .addon-mod_feedback-form-content { | ||||
|         @include margin($item-md-padding-media-top, ($item-md-padding-end / 2), $item-md-padding-media-bottom, 0); | ||||
|     } | ||||
|     .item-ios .addon-mod_feedback-form-content { | ||||
|         @include margin($item-ios-padding-media-top, $item-ios-padding-start, $item-ios-padding-media-bottom, 0); | ||||
|     } | ||||
|     .item-wp .addon-mod_feedback-form-content { | ||||
|         @include margin($item-wp-padding-media-top, ($item-wp-padding-end / 2), $item-wp-padding-media-bottom, 0); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										341
									
								
								src/addon/mod/feedback/pages/form/form.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								src/addon/mod/feedback/pages/form/form.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,341 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnDestroy, Optional } from '@angular/core'; | ||||
| import { IonicPage, NavParams, NavController, Content } from 'ionic-angular'; | ||||
| import { Network } from '@ionic-native/network'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { AddonModFeedbackProvider } from '../../providers/feedback'; | ||||
| import { AddonModFeedbackHelperProvider } from '../../providers/helper'; | ||||
| import { AddonModFeedbackSyncProvider } from '../../providers/sync'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreLoginHelperProvider } from '@core/login/providers/helper'; | ||||
| import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays feedback form. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-feedback-form' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-feedback-form', | ||||
|     templateUrl: 'form.html', | ||||
| }) | ||||
| export class AddonModFeedbackFormPage implements OnDestroy { | ||||
| 
 | ||||
|     protected module: any; | ||||
|     protected currentPage: number; | ||||
|     protected submitted: any; | ||||
|     protected feedback; | ||||
|     protected siteAfterSubmit; | ||||
|     protected onlineObserver; | ||||
|     protected originalData; | ||||
|     protected currentSite; | ||||
|     protected forceLeave = false; | ||||
| 
 | ||||
|     title: string; | ||||
|     preview = false; | ||||
|     courseId: number; | ||||
|     componentId: number; | ||||
|     completionPageContents: string; | ||||
|     component = AddonModFeedbackProvider.COMPONENT; | ||||
|     offline = false; | ||||
|     feedbackLoaded = false; | ||||
|     access: any; | ||||
|     items = []; | ||||
|     hasPrevPage = false; | ||||
|     hasNextPage = false; | ||||
|     completed = false; | ||||
|     completedOffline = false; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, protected feedbackProvider: AddonModFeedbackProvider, protected appProvider: CoreAppProvider, | ||||
|             protected utils: CoreUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected navCtrl: NavController, | ||||
|             protected feedbackHelper: AddonModFeedbackHelperProvider, protected courseProvider: CoreCourseProvider, | ||||
|             protected eventsProvider: CoreEventsProvider, protected feedbackSync: AddonModFeedbackSyncProvider, network: Network, | ||||
|             protected translate: TranslateService, protected loginHelper: CoreLoginHelperProvider, | ||||
|             protected linkHelper: CoreContentLinksHelperProvider, sitesProvider: CoreSitesProvider, | ||||
|             @Optional() private content: Content) { | ||||
| 
 | ||||
|         this.module = navParams.get('module'); | ||||
|         this.courseId = navParams.get('courseId'); | ||||
|         this.currentPage = navParams.get('page'); | ||||
|         this.title = navParams.get('title'); | ||||
|         this.preview = !!navParams.get('preview'); | ||||
|         this.componentId = navParams.get('moduleId') || this.module.id; | ||||
| 
 | ||||
|         this.currentSite = sitesProvider.getCurrentSite(); | ||||
| 
 | ||||
|         // Refresh online status when changes.
 | ||||
|         this.onlineObserver = network.onchange().subscribe((online) => { | ||||
|             this.offline = !online; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View loaded. | ||||
|      */ | ||||
|     ionViewDidLoad(): void { | ||||
|         this.fetchData().then(() => { | ||||
|             this.feedbackProvider.logView(this.feedback.id, true).then(() => { | ||||
|                 this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View entered. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         this.forceLeave = false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return {boolean | Promise<void>} Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     ionViewCanLeave(): boolean | Promise<void> { | ||||
|         if (this.forceLeave) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (!this.preview) { | ||||
|             const responses = this.feedbackHelper.getPageItemsResponses(this.items); | ||||
| 
 | ||||
|             if (this.items && !this.completed && this.originalData) { | ||||
|                 // Form submitted. Check if there is any change.
 | ||||
|                 if (!this.utils.basicLeftCompare(responses, this.originalData, 3)) { | ||||
|                      return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch all the data required for the view. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchData(): Promise<any> { | ||||
|         this.offline = !this.appProvider.isOnline(); | ||||
| 
 | ||||
|         return this.feedbackProvider.getFeedback(this.courseId, this.module.id).then((feedbackData) => { | ||||
|             this.feedback = feedbackData; | ||||
| 
 | ||||
|             this.title = this.feedback.name || this.title; | ||||
| 
 | ||||
|             return this.fetchAccessData(); | ||||
|         }).then((accessData) => { | ||||
|             if (!this.preview && accessData.cansubmit && !accessData.isempty) { | ||||
|                 return typeof this.currentPage == 'undefined' ? | ||||
|                     this.feedbackProvider.getResumePage(this.feedback.id, this.offline, true) : | ||||
|                     Promise.resolve(this.currentPage); | ||||
|             } else { | ||||
|                 this.preview = true; | ||||
| 
 | ||||
|                 return Promise.resolve(0); | ||||
|             } | ||||
|         }).catch((error) => { | ||||
|             if (!this.offline && !this.utils.isWebServiceError(error)) { | ||||
|                 // If it fails, go offline.
 | ||||
|                 this.offline = true; | ||||
| 
 | ||||
|                 return this.feedbackProvider.getResumePage(this.feedback.id, true); | ||||
|             } | ||||
| 
 | ||||
|             return Promise.reject(error); | ||||
|         }).then((page) => { | ||||
|             return this.fetchFeedbackPageData(page || 0); | ||||
|         }).catch((message) => { | ||||
|             this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); | ||||
|             this.forceLeave = true; | ||||
|             this.navCtrl.pop(); | ||||
| 
 | ||||
|             return Promise.reject(null); | ||||
|         }).finally(() => { | ||||
|             this.feedbackLoaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch access information. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchAccessData(): Promise<any> { | ||||
|         return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, this.offline, true).catch((error) => { | ||||
|             if (!this.offline && !this.utils.isWebServiceError(error)) { | ||||
|                 // If it fails, go offline.
 | ||||
|                 this.offline = true; | ||||
| 
 | ||||
|                 return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, true); | ||||
|             } | ||||
| 
 | ||||
|             return Promise.reject(error); | ||||
|          }).then((accessData) => { | ||||
|             this.access = accessData; | ||||
| 
 | ||||
|             return accessData; | ||||
|          }); | ||||
|     } | ||||
| 
 | ||||
|     protected fetchFeedbackPageData(page: number = 0): Promise<void> { | ||||
|         let promise; | ||||
|         this.items = []; | ||||
| 
 | ||||
|         if (this.preview) { | ||||
|             promise = this.feedbackProvider.getItems(this.feedback.id); | ||||
|         } else { | ||||
|             this.currentPage = page; | ||||
| 
 | ||||
|             promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, this.offline, true).catch((error) => { | ||||
|                 if (!this.offline && !this.utils.isWebServiceError(error)) { | ||||
|                     // If it fails, go offline.
 | ||||
|                     this.offline = true; | ||||
| 
 | ||||
|                     return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, true); | ||||
|                 } | ||||
| 
 | ||||
|                 return Promise.reject(error); | ||||
|             }).then((response) => { | ||||
|                 this.hasPrevPage = !!response.hasprevpage; | ||||
|                 this.hasNextPage = !!response.hasnextpage; | ||||
| 
 | ||||
|                 return response; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return promise.then((response) => { | ||||
|             this.items = response.items.map((itemData) => { | ||||
|                 return this.feedbackHelper.getItemForm(itemData, this.preview); | ||||
|             }).filter((itemData) => { | ||||
|                 // Filter items with errors.
 | ||||
|                 return itemData; | ||||
|             }); | ||||
| 
 | ||||
|             if (!this.preview) { | ||||
|                 const itemsCopy = this.utils.clone(this.items); // Copy the array to avoid modifications.
 | ||||
|                 this.originalData = this.feedbackHelper.getPageItemsResponses(itemsCopy); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function to allow page navigation through the questions form. | ||||
|      * | ||||
|      * @param  {boolean}       goPrevious If true it will go back to the previous page, if false, it will go forward. | ||||
|      * @return {Promise<void>}            Resolved when done. | ||||
|      */ | ||||
|     gotoPage(goPrevious: boolean): Promise<void> { | ||||
|         this.content && this.content.scrollToTop(); | ||||
|         this.feedbackLoaded = false; | ||||
| 
 | ||||
|         const responses = this.feedbackHelper.getPageItemsResponses(this.items), | ||||
|             formHasErrors = this.items.some((item) => { | ||||
|                 return item.isEmpty || item.hasError; | ||||
|             }); | ||||
| 
 | ||||
|         // Sync other pages first.
 | ||||
|         return this.feedbackSync.syncFeedback(this.feedback.id).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         }).then(() => { | ||||
|             return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, goPrevious, formHasErrors, | ||||
|                     this.courseId).then((response) => { | ||||
|                 const jumpTo = parseInt(response.jumpto, 10); | ||||
| 
 | ||||
|                 if (response.completed) { | ||||
|                     // Form is completed, show completion message and buttons.
 | ||||
|                     this.items = []; | ||||
|                     this.completed = true; | ||||
|                     this.completedOffline = !!response.offline; | ||||
|                     this.completionPageContents = response.completionpagecontents; | ||||
|                     this.siteAfterSubmit = response.siteaftersubmit; | ||||
|                     this.submitted = true; | ||||
| 
 | ||||
|                     // Invalidate access information so user will see home page updated (continue form or completion messages).
 | ||||
|                     const promises = []; | ||||
|                     promises.push(this.feedbackProvider.invalidateFeedbackAccessInformationData(this.feedback.id)); | ||||
|                     promises.push(this.feedbackProvider.invalidateResumePageData(this.feedback.id)); | ||||
| 
 | ||||
|                     return Promise.all(promises).then(() => { | ||||
|                         return this.fetchAccessData(); | ||||
|                     }); | ||||
|                 } else if (isNaN(jumpTo) || jumpTo == this.currentPage) { | ||||
|                     // Errors on questions, stay in page.
 | ||||
|                     return Promise.resolve(); | ||||
|                 } else { | ||||
|                     this.submitted = true; | ||||
|                     // Invalidate access information so user will see home page updated (continue form).
 | ||||
|                     this.feedbackProvider.invalidateResumePageData(this.feedback.id); | ||||
| 
 | ||||
|                     // Fetch the new page.
 | ||||
|                     return this.fetchFeedbackPageData(jumpTo); | ||||
|                 } | ||||
|             }); | ||||
|         }).catch((message) => { | ||||
|             this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); | ||||
| 
 | ||||
|             return Promise.reject(null); | ||||
|         }).finally(() => { | ||||
|             this.feedbackLoaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function to link implemented features. | ||||
|      */ | ||||
|     showAnalysis(): void { | ||||
|         this.submitted = 'analysis'; | ||||
|         this.feedbackHelper.openFeature('analysis', this.navCtrl, this.module, this.courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function to go to the page after submit. | ||||
|      */ | ||||
|     continue(): void { | ||||
|         if (this.siteAfterSubmit) { | ||||
|             const modal = this.domUtils.showModalLoading(); | ||||
|             this.linkHelper.handleLink(this.siteAfterSubmit).then((treated) => { | ||||
|                 if (!treated) { | ||||
|                     return this.currentSite.openInBrowserWithAutoLoginIfSameSite(this.siteAfterSubmit); | ||||
|                 } | ||||
|             }).finally(() => { | ||||
|                 modal.dismiss(); | ||||
|             }); | ||||
|         } else { | ||||
|             // Use redirect to make the course the new history root (to avoid "loops" in history).
 | ||||
|             this.loginHelper.redirect('CoreCourseSectionPage', { | ||||
|                 course: { id: this.courseId } | ||||
|             }, this.currentSite.getId()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         if (this.submitted) { | ||||
|             const tab = this.submitted == 'analysis' ? 'analysis' : 'overview'; | ||||
|             // If form has been submitted, the info has been already invalidated but we should update index view.
 | ||||
|             this.eventsProvider.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, {feedbackId: this.feedback.id, tab: tab}); | ||||
|         } | ||||
|         this.onlineObserver && this.onlineObserver.unsubscribe(); | ||||
|     } | ||||
| } | ||||
| @ -17,6 +17,8 @@ import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { AddonModFeedbackOfflineProvider } from './offline'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features for feedbacks. | ||||
| @ -35,10 +37,255 @@ export class AddonModFeedbackProvider { | ||||
|     protected logger; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, | ||||
|             private filepoolProvider: CoreFilepoolProvider) { | ||||
|             private filepoolProvider: CoreFilepoolProvider, private feedbackOffline: AddonModFeedbackOfflineProvider, | ||||
|             private appProvider: CoreAppProvider) { | ||||
|         this.logger = logger.getInstance('AddonModFeedbackProvider'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check dependency of a question item. | ||||
|      * | ||||
|      * @param   {any[]}  items      All question items to check dependency. | ||||
|      * @param   {any}    item       Item to check. | ||||
|      * @return  {boolean}           Return true if dependency is acomplished and it can be shown. False, otherwise. | ||||
|      */ | ||||
|     protected checkDependencyItem(items: any[], item: any): boolean { | ||||
|         const depend = items.find((itemFind) => { | ||||
|             return itemFind.id == item.dependitem; | ||||
|         }); | ||||
| 
 | ||||
|         // Item not found, looks like dependent item has been removed or is in the same or following pages.
 | ||||
|         if (!depend) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         switch (depend.typ) { | ||||
|             case 'label': | ||||
|                 return false; | ||||
|             case 'multichoice': | ||||
|             case 'multichoicerated': | ||||
|                 return this.compareDependItemMultichoice(depend, item.dependvalue); | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
|         return item.dependvalue == depend.rawValue; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check dependency item of type Multichoice. | ||||
|      * | ||||
|      * @param  {any}     item        Item to check. | ||||
|      * @param  {string}  dependValue Value to compare. | ||||
|      * @return {boolean}             eturn true if dependency is acomplished and it can be shown. False, otherwise. | ||||
|      */ | ||||
|     protected compareDependItemMultichoice(item: any, dependValue: string): boolean { | ||||
|         let values, choices; | ||||
|         const parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP) || [], | ||||
|             subtype = parts.length > 0 && parts[0] ? parts[0] : 'r'; | ||||
| 
 | ||||
|         choices = parts[1] || ''; | ||||
|         choices = choices.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP)[0] || ''; | ||||
|         choices = choices.split(AddonModFeedbackProvider.LINE_SEP) || []; | ||||
| 
 | ||||
|         if (subtype === 'c') { | ||||
|             if (typeof item.rawValue == 'undefined') { | ||||
|                 values = ['']; | ||||
|             } else { | ||||
|                 item.rawValue = '' + item.rawValue; | ||||
|                 values = item.rawValue.split(AddonModFeedbackProvider.LINE_SEP); | ||||
|             } | ||||
|         } else { | ||||
|             values = [item.rawValue]; | ||||
|         } | ||||
| 
 | ||||
|         for (let index = 0; index < choices.length; index++) { | ||||
|             for (const x in values) { | ||||
|                 if (values[x] == index + 1) { | ||||
|                     let value = choices[index]; | ||||
| 
 | ||||
|                     if (item.typ == 'multichoicerated') { | ||||
|                         value = value.split(AddonModFeedbackProvider.MULTICHOICERATED_VALUE_SEP)[1] || ''; | ||||
|                     } | ||||
| 
 | ||||
|                     if (value.trim() == dependValue) { | ||||
|                         return true; | ||||
|                     } | ||||
| 
 | ||||
|                     // We can finish checking if only searching on one value and we found it.
 | ||||
|                     if (values.length == 1) { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fill values of item questions. | ||||
|      * | ||||
|      * @param   {number}   feedbackId   Feedback ID. | ||||
|      * @param   {any[]}    items        Item to fill the value. | ||||
|      * @param   {boolean}  offline      True if it should return cached data. Has priority over ignoreCache. | ||||
|      * @param   {boolean}  ignoreCache  True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param   {string}   siteId       Site ID. | ||||
|      * @return  {Promise<any>}          Resolved with values when done. | ||||
|      */ | ||||
|     protected fillValues(feedbackId: number, items: any[], offline: boolean, ignoreCache: boolean, siteId: string): Promise<any> { | ||||
|         return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId).then((valuesArray) => { | ||||
|             if (valuesArray.length == 0) { | ||||
|                 // Try sending empty values to get the last completed attempt values.
 | ||||
|                 return this.processPageOnline(feedbackId, 0, {}, undefined, siteId).then(() => { | ||||
|                     return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId); | ||||
|                 }).catch(() => { | ||||
|                     // Ignore errors
 | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return valuesArray; | ||||
| 
 | ||||
|         }).then((valuesArray) => { | ||||
|             const values = {}; | ||||
| 
 | ||||
|             valuesArray.forEach((value) => { | ||||
|                 values[value.item] = value.value; | ||||
|             }); | ||||
| 
 | ||||
|             items.forEach((itemData) => { | ||||
|                 if (itemData.hasvalue && typeof values[itemData.id] != 'undefined') { | ||||
|                     itemData.rawValue = values[itemData.id]; | ||||
|                 } | ||||
|             }); | ||||
|         }).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         }).then(() => { | ||||
|             // Merge with offline data.
 | ||||
|             return this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).then((offlineValuesArray) => { | ||||
|                 const offlineValues = {}; | ||||
| 
 | ||||
|                 // Merge all values into one array.
 | ||||
|                 offlineValuesArray = offlineValuesArray.reduce((a, b) => { | ||||
|                     const responses = this.utils.objectToArrayOfObjects(b.responses, 'id', 'value'); | ||||
| 
 | ||||
|                     return a.concat(responses); | ||||
|                 }, []).map((a) => { | ||||
|                     const parts = a.id.split('_'); | ||||
|                     a.typ = parts[0]; | ||||
|                     a.item = parseInt(parts[1], 0); | ||||
| 
 | ||||
|                     return a; | ||||
|                 }); | ||||
| 
 | ||||
|                 offlineValuesArray.forEach((value) => { | ||||
|                     if (typeof offlineValues[value.item] == 'undefined') { | ||||
|                         offlineValues[value.item] = []; | ||||
|                     } | ||||
|                     offlineValues[value.item].push(value.value); | ||||
|                 }); | ||||
| 
 | ||||
|                 items.forEach((itemData) => { | ||||
|                     if (itemData.hasvalue && typeof offlineValues[itemData.id] != 'undefined') { | ||||
|                         // Treat multichoice checkboxes.
 | ||||
|                         if (itemData.typ == 'multichoice' && | ||||
|                                 itemData.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP)[0] == 'c') { | ||||
| 
 | ||||
|                             offlineValues[itemData.id] = offlineValues[itemData.id].filter((value) => { | ||||
|                                 return value > 0; | ||||
|                             }); | ||||
|                             itemData.rawValue = offlineValues[itemData.id].join(AddonModFeedbackProvider.LINE_SEP); | ||||
|                         } else { | ||||
|                             itemData.rawValue = offlineValues[itemData.id][0]; | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 return items; | ||||
|             }); | ||||
|         }).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|             return items; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns all the feedback non respondents users. | ||||
|      * | ||||
|      * @param   {number}    feedbackId      Feedback ID. | ||||
|      * @param   {number}    groupId         Group id, 0 means that the function will determine the user group. | ||||
|      * @param   {string}    [siteId]        Site ID. If not defined, current site. | ||||
|      * @param   {any}       [previous]      Only for recurrent use. Object with the previous fetched info. | ||||
|      * @return  {Promise<any>}              Promise resolved when the info is retrieved. | ||||
|      */ | ||||
|     getAllNonRespondents(feedbackId: number, groupId: number, siteId?: string, previous?: any): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
|         if (typeof previous == 'undefined') { | ||||
|             previous = { | ||||
|                 page: 0, | ||||
|                 users: [] | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         return this.getNonRespondents(feedbackId, groupId, previous.page, siteId).then((response) => { | ||||
|             if (previous.users.length < response.total) { | ||||
|                 previous.users = previous.users.concat(response.users); | ||||
|             } | ||||
| 
 | ||||
|             if (previous.users.length < response.total) { | ||||
|                 // Can load more.
 | ||||
|                 previous.page++; | ||||
| 
 | ||||
|                 return this.getAllNonRespondents(feedbackId, groupId, siteId, previous); | ||||
|             } | ||||
|             previous.total = response.total; | ||||
| 
 | ||||
|             return previous; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns all the feedback user responses. | ||||
|      * | ||||
|      * @param   {number}    feedbackId      Feedback ID. | ||||
|      * @param   {number}    groupId         Group id, 0 means that the function will determine the user group. | ||||
|      * @param   {string}    [siteId]        Site ID. If not defined, current site. | ||||
|      * @param   {any}       [previous]      Only for recurrent use. Object with the previous fetched info. | ||||
|      * @return  {Promise<any>}              Promise resolved when the info is retrieved. | ||||
|      */ | ||||
|     getAllResponsesAnalysis(feedbackId: number, groupId: number, siteId?: string, previous?: any): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
|         if (typeof previous == 'undefined') { | ||||
|             previous = { | ||||
|                 page: 0, | ||||
|                 attempts: [], | ||||
|                 anonattempts: [] | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         return this.getResponsesAnalysis(feedbackId, groupId, previous.page, siteId).then((responses) => { | ||||
|             if (previous.anonattempts.length < responses.totalanonattempts) { | ||||
|                 previous.anonattempts = previous.anonattempts.concat(responses.anonattempts); | ||||
|             } | ||||
| 
 | ||||
|             if (previous.attempts.length < responses.totalattempts) { | ||||
|                 previous.attempts = previous.attempts.concat(responses.attempts); | ||||
|             } | ||||
| 
 | ||||
|             if (previous.anonattempts.length < responses.totalanonattempts || previous.attempts.length < responses.totalattempts) { | ||||
|                 // Can load more.
 | ||||
|                 previous.page++; | ||||
| 
 | ||||
|                 return this.getAllResponsesAnalysis(feedbackId, groupId, siteId, previous); | ||||
|             } | ||||
| 
 | ||||
|             previous.totalattempts = responses.totalattempts; | ||||
|             previous.totalanonattempts = responses.totalanonattempts; | ||||
| 
 | ||||
|             return previous; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get analysis information for a given feedback. | ||||
|      * | ||||
| @ -436,6 +683,118 @@ export class AddonModFeedbackProvider { | ||||
|         return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':nonrespondents:'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a single feedback page items. This function is not cached, use AddonModFeedbackHelperProvider#getPageItems instead. | ||||
|      * | ||||
|      * @param   {number}    feedbackId  Feedback ID. | ||||
|      * @param   {number}    page        The page to get. | ||||
|      * @param   {string}    [siteId]    Site ID. If not defined, current site. | ||||
|      * @return  {Promise<any>}          Promise resolved when the info is retrieved. | ||||
|      */ | ||||
|     getPageItems(feedbackId: number, page: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                     feedbackid: feedbackId, | ||||
|                     page: page | ||||
|                 }; | ||||
| 
 | ||||
|             return site.write('mod_feedback_get_page_items', params); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a single feedback page items. If offline or server down it will use getItems to calculate dependencies. | ||||
|      * | ||||
|      * @param   {number}  feedbackId          Feedback ID. | ||||
|      * @param   {number}  page                The page to get. | ||||
|      * @param   {boolean} [offline=false]     True if it should return cached data. Has priority over ignoreCache. | ||||
|      * @param   {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param   {string}  [siteId]            Site ID. If not defined, current site. | ||||
|      * @return  {Promise<any>}                Promise resolved when the info is retrieved. | ||||
|      */ | ||||
|     getPageItemsWithValues(feedbackId: number, page: number, offline: boolean = false, ignoreCache: boolean = false, | ||||
|             siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         return this.getPageItems(feedbackId, page, siteId).then((response) => { | ||||
|             return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { | ||||
|                 response.items = items; | ||||
| 
 | ||||
|                 return response; | ||||
|             }); | ||||
|         }).catch(() => { | ||||
|             // If getPageItems fail we should calculate it using getItems.
 | ||||
|             return this.getItems(feedbackId, siteId).then((response) => { | ||||
|                 return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { | ||||
|                     // Separate items by pages.
 | ||||
|                     let currentPage = 0; | ||||
|                     const previousPageItems = []; | ||||
| 
 | ||||
|                     const pageItems = items.filter((item) => { | ||||
|                         // Greater page, discard all entries.
 | ||||
|                         if (currentPage > page) { | ||||
|                             return false; | ||||
|                         } | ||||
| 
 | ||||
|                         if (item.typ == 'pagebreak') { | ||||
|                             currentPage++; | ||||
| 
 | ||||
|                             return false; | ||||
|                         } | ||||
| 
 | ||||
|                         // Save items on previous page to check dependencies and discard entry.
 | ||||
|                         if (currentPage < page) { | ||||
|                             previousPageItems.push(item); | ||||
| 
 | ||||
|                             return false; | ||||
|                         } | ||||
| 
 | ||||
|                         // Filter depending items.
 | ||||
|                         if (item && item.dependitem > 0 && previousPageItems.length > 0) { | ||||
|                             return this.checkDependencyItem(previousPageItems, item); | ||||
|                         } | ||||
| 
 | ||||
|                         // Filter items with errors.
 | ||||
|                         return item; | ||||
|                     }); | ||||
| 
 | ||||
|                     // Check if there are more pages.
 | ||||
|                     response.hasprevpage = page > 0; | ||||
|                     response.hasnextpage = currentPage > page; | ||||
|                     response.items = pageItems; | ||||
| 
 | ||||
|                     return response; | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to get the page we can jump. | ||||
|      * | ||||
|      * @param  {number}  feedbackId [description] | ||||
|      * @param  {number}  page       [description] | ||||
|      * @param  {number}  changePage [description] | ||||
|      * @param  {string}  siteId     [description] | ||||
|      * @return {Promise<number | false>}            [description] | ||||
|      */ | ||||
|     protected getPageJumpTo(feedbackId: number, page: number, changePage: number, siteId: string): Promise<number | false> { | ||||
|         return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => { | ||||
|             // The page we are going has items.
 | ||||
|             if (resp.items.length > 0) { | ||||
|                 return page; | ||||
|             } | ||||
| 
 | ||||
|             // Check we can jump futher.
 | ||||
|             if ((changePage == 1 && resp.hasnextpage) || (changePage == -1 && resp.hasprevpage)) { | ||||
|                 return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId); | ||||
|             } | ||||
| 
 | ||||
|             // Completed or first page.
 | ||||
|             return false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the feedback user responses. | ||||
|      * | ||||
| @ -723,6 +1082,93 @@ export class AddonModFeedbackProvider { | ||||
|         return this.sitesProvider.getCurrentSite().write('mod_feedback_view_feedback', params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process a jump between pages. | ||||
|      * | ||||
|      * @param   {number}    feedbackId      Feedback ID. | ||||
|      * @param   {number}    page            The page being processed. | ||||
|      * @param   {any}       responses       The data to be processed the key is the field name (usually type[index]_id). | ||||
|      * @param   {boolean}   goPrevious      Whether we want to jump to previous page. | ||||
|      * @param   {boolean}   formHasErrors   Whether the form we sent has required but empty fields (only used in offline). | ||||
|      * @param   {number}    courseId        Course ID the feedback belongs to. | ||||
|      * @param   {string}    [siteId]        Site ID. If not defined, current site. | ||||
|      * @return  {Promise<any>}                   Promise resolved when the info is retrieved. | ||||
|      */ | ||||
|     processPage(feedbackId: number, page: number, responses: any, goPrevious: boolean, formHasErrors: boolean, courseId: number, | ||||
|             siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a message to be synchronized later.
 | ||||
|         const storeOffline = (): Promise<any> => { | ||||
|             return this.feedbackOffline.saveResponses(feedbackId, page, responses, courseId, siteId).then(() => { | ||||
|                 // Simulate process_page response.
 | ||||
|                 const response = { | ||||
|                         jumpto: page, | ||||
|                         completed: false, | ||||
|                         offline: true | ||||
|                     }; | ||||
|                 let changePage = 0; | ||||
| 
 | ||||
|                 if (goPrevious) { | ||||
|                     if (page > 0) { | ||||
|                         changePage = -1; | ||||
|                     } | ||||
|                 } else if (!formHasErrors) { | ||||
|                     // We can only go next if it has no errors.
 | ||||
|                     changePage = 1; | ||||
|                 } | ||||
| 
 | ||||
|                 if (changePage === 0) { | ||||
|                     return response; | ||||
|                 } | ||||
| 
 | ||||
|                 return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => { | ||||
|                     // Check completion.
 | ||||
|                     if (changePage == 1 && !resp.hasnextpage) { | ||||
|                         response.completed = true; | ||||
| 
 | ||||
|                         return response; | ||||
|                     } | ||||
| 
 | ||||
|                     return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId).then((loadPage) => { | ||||
|                         if (loadPage === false) { | ||||
|                             // Completed or first page.
 | ||||
|                             if (changePage == -1) { | ||||
|                                 // First page.
 | ||||
|                                 response.jumpto = 0; | ||||
|                             } else { | ||||
|                                 // Completed.
 | ||||
|                                 response.completed = true; | ||||
|                             } | ||||
|                         } else { | ||||
|                             response.jumpto = loadPage; | ||||
|                         } | ||||
| 
 | ||||
|                         return response; | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         if (!this.appProvider.isOnline()) { | ||||
|             // App is offline, store the action.
 | ||||
|             return storeOffline(); | ||||
|         } | ||||
| 
 | ||||
|         // If there's already a response to be sent to the server, discard it first.
 | ||||
|         return this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, siteId).then(() => { | ||||
|             return this.processPageOnline(feedbackId, page, responses, goPrevious, siteId).catch((error) => { | ||||
|                 if (this.utils.isWebServiceError(error)) { | ||||
|                     // The WebService has thrown an error, this means that responses cannot be submitted.
 | ||||
|                     return Promise.reject(error); | ||||
|                 } | ||||
| 
 | ||||
|                 // Couldn't connect to server, store in offline.
 | ||||
|                 return storeOffline(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process a jump between pages. | ||||
|      * | ||||
|  | ||||
| @ -94,6 +94,88 @@ export class AddonModFeedbackHelperProvider { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get page items responses to be sent. | ||||
|      * | ||||
|      * @param   {any[]} items    Items where the values are. | ||||
|      * @return  {any}            Responses object to be sent. | ||||
|      */ | ||||
|     getPageItemsResponses(items: any[]): any { | ||||
|         const responses = {}; | ||||
| 
 | ||||
|         items.forEach((itemData) => { | ||||
|             let answered = false; | ||||
| 
 | ||||
|             itemData.hasError = false; | ||||
| 
 | ||||
|             if (itemData.typ == 'captcha') { | ||||
|                 const value = itemData.value || '', | ||||
|                     name = itemData.typ + '_' + itemData.id; | ||||
| 
 | ||||
|                 answered = !!value; | ||||
|                 responses[name] = 1; | ||||
|                 responses['g-recaptcha-response'] = value; | ||||
|                 responses['recaptcha_element'] = 'dummyvalue'; | ||||
| 
 | ||||
|                 if (itemData.required && !answered) { | ||||
|                     // Check if it has any value.
 | ||||
|                     itemData.isEmpty = true; | ||||
|                 } else { | ||||
|                     itemData.isEmpty = false; | ||||
|                 } | ||||
|             } else if (itemData.hasvalue) { | ||||
|                 let name, value; | ||||
|                 const nameTemp = itemData.typ + '_' + itemData.id; | ||||
| 
 | ||||
|                 if (itemData.typ == 'multichoice' && itemData.subtype == 'c') { | ||||
|                     name = nameTemp + '[0]'; | ||||
|                     responses[name] = 0; | ||||
|                     itemData.choices.forEach((choice, index) => { | ||||
|                         name = nameTemp + '[' + (index + 1) + ']'; | ||||
|                         value = choice.checked ? choice.value : 0; | ||||
|                         if (!answered && value) { | ||||
|                             answered = true; | ||||
|                         } | ||||
|                         responses[name] = value; | ||||
|                     }); | ||||
|                 } else { | ||||
|                     if (itemData.typ == 'multichoice') { | ||||
|                         name = nameTemp + '[0]'; | ||||
|                     } else { | ||||
|                         name = nameTemp; | ||||
|                     } | ||||
| 
 | ||||
|                     if (itemData.typ == 'multichoice' || itemData.typ == 'multichoicerated') { | ||||
|                         value = itemData.value || 0; | ||||
|                     } else if (itemData.typ == 'numeric') { | ||||
|                         value = itemData.value || itemData.value  == 0 ? itemData.value : ''; | ||||
| 
 | ||||
|                         if (value != '') { | ||||
|                             if ((itemData.rangefrom != '' && value < itemData.rangefrom) || | ||||
|                                     (itemData.rangeto != '' && value > itemData.rangeto)) { | ||||
|                                 itemData.hasError = true; | ||||
|                             } | ||||
|                         } | ||||
|                     } else { | ||||
|                         value = itemData.value || itemData.value  == 0 ? itemData.value : ''; | ||||
|                     } | ||||
| 
 | ||||
|                     answered = !!value; | ||||
|                     responses[name] = value; | ||||
|                 } | ||||
| 
 | ||||
|                 if (itemData.required && !answered) { | ||||
|                     // Check if it has any value.
 | ||||
|                     itemData.isEmpty = true; | ||||
|                 } else { | ||||
|                     itemData.isEmpty = false; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         return responses; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the feedback user responses with extra info. | ||||
|      * | ||||
| @ -269,8 +351,6 @@ export class AddonModFeedbackHelperProvider { | ||||
|             parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP) || []; | ||||
|             item.presentation = parts.length > 0 ? parts[0] : ''; | ||||
|             // Horizontal are not supported right now. item.horizontal = parts.length > 1 && !!parts[1];
 | ||||
|         } else { | ||||
|             item.class = 'item-select'; | ||||
|         } | ||||
| 
 | ||||
|         item.choices = item.presentation.split(AddonModFeedbackProvider.LINE_SEP) || []; | ||||
| @ -320,9 +400,6 @@ export class AddonModFeedbackHelperProvider { | ||||
|         const data = this.textUtils.parseJSON(item.otherdata); | ||||
|         if (data && data.length > 3) { | ||||
|             item.captcha = { | ||||
|                 challengehash: data[0], | ||||
|                 imageurl: data[1], | ||||
|                 jsurl: data[2], | ||||
|                 recaptchapublickey: data[3] | ||||
|             }; | ||||
|         } | ||||
|  | ||||
| @ -157,7 +157,7 @@ export class AddonModFeedbackOfflineProvider { | ||||
|                     timemodified: this.timeUtils.timestamp() | ||||
|                 }; | ||||
| 
 | ||||
|             return site.getDb().insertOrUpdateRecord(this.FEEDBACK_TABLE, entry, {feedbackid: feedbackId, page: page}); | ||||
|             return site.getDb().insertRecord(this.FEEDBACK_TABLE, entry); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -47,7 +47,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan | ||||
|      *                           in the filepool root feedback. | ||||
|      * @return {Promise<any>} Promise resolved when all content is downloaded. Data returned is not reliable. | ||||
|      */ | ||||
|     /*downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise<any> { | ||||
|     downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise<any> { | ||||
|         const promises = [], | ||||
|             siteId = this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
| @ -119,7 +119,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan | ||||
|         })); | ||||
| 
 | ||||
|         return Promise.all(promises); | ||||
|     }*/ | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the list of downloadable files. | ||||
| @ -129,7 +129,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan | ||||
|      * @param  {string} [siteId]  Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}     Promise resolved with the list of files. | ||||
|      */ | ||||
|     /*getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> { | ||||
|     getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> { | ||||
|         let files = []; | ||||
| 
 | ||||
|         return this.feedbackProvider.getFeedback(courseId, module.id).then((feedback) => { | ||||
| @ -149,7 +149,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan | ||||
|             // Any error, return the list we have.
 | ||||
|             return files; | ||||
|         }); | ||||
|     }*/ | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns feedback intro files. | ||||
|  | ||||
| @ -567,8 +567,17 @@ textarea { | ||||
|     height: auto; | ||||
| } | ||||
| 
 | ||||
| // Message cards | ||||
| canvas[core-chart] { | ||||
|   max-width: 500px; | ||||
|   margin: 0 auto; | ||||
| } | ||||
| 
 | ||||
| .core-circle:before { | ||||
|   content: ' \25CF'; | ||||
| } | ||||
| 
 | ||||
| @each $color-name, $color-base, $color-contrast in get-colors($colors) { | ||||
|   // Message cards. | ||||
|   .core-#{$color-name}-card { | ||||
|     @extend ion-card; | ||||
|     border-bottom: 3px solid $color-base; | ||||
| @ -589,21 +598,18 @@ textarea { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| canvas[core-chart] { | ||||
|   max-width: 500px; | ||||
|   margin: 0 auto; | ||||
| } | ||||
|   .core-#{$color-name}-item { | ||||
|     border-bottom: 3px solid $color-base !important; | ||||
|     ion-icon { | ||||
|       color: $color-base; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| .core-circle:before { | ||||
|   content: ' \25CF'; | ||||
| } | ||||
| 
 | ||||
| @each $color-name, $color-base, $color-contrast in get-colors($colors) { | ||||
|   .core-#{$color-name}-circle { | ||||
|     margin: 0 4px; | ||||
|   } | ||||
| 
 | ||||
|   .core-#{$color-name}-circle:before { | ||||
|     @extend .core-circle:before; | ||||
|     color: $color-base; | ||||
|  | ||||
| @ -59,7 +59,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
| 
 | ||||
|         // Refresh online status when changes.
 | ||||
|         this.onlineObserver = network.onchange().subscribe((online) => { | ||||
|             this.isOnline = this.appProvider.isOnline(); | ||||
|             this.isOnline = online; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -170,9 +170,10 @@ export class CoreUtilsProvider { | ||||
| 
 | ||||
|     /** | ||||
|      * Blocks leaving a view. | ||||
|      * @deprecated, use ionViewCanLeave instead. | ||||
|      */ | ||||
|     blockLeaveView(): void { | ||||
|         // @todo
 | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user