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. |      * @param {boolean} preview Preview or edit the form. | ||||||
|      */ |      */ | ||||||
|     gotoAnswerQuestions(preview: boolean): void { |     gotoAnswerQuestions(preview: boolean = false): void { | ||||||
|         const stateParams = { |         const stateParams = { | ||||||
|             module: this.module, |             module: this.module, | ||||||
|             moduleid: this.module.id, |             moduleId: this.module.id, | ||||||
|             courseid: this.courseId, |             courseId: this.courseId, | ||||||
|             preview: preview |             preview: preview | ||||||
|         }; |         }; | ||||||
|         this.navCtrl.push('AddonModFeedbackFormPage', stateParams); |         this.navCtrl.push('AddonModFeedbackFormPage', stateParams); | ||||||
|  | |||||||
| @ -3,24 +3,32 @@ | |||||||
|     "anonymous": "Anonymous", |     "anonymous": "Anonymous", | ||||||
|     "anonymous_entries": "Anonymous entries ({{$a}})", |     "anonymous_entries": "Anonymous entries ({{$a}})", | ||||||
|     "average": "Average", |     "average": "Average", | ||||||
|  |     "captchaofflinewarning": "Feedback with CAPTCHA cannot be completed offline, or if not configured, or if the server is down.", | ||||||
|     "completed_feedbacks": "Submitted answers", |     "completed_feedbacks": "Submitted answers", | ||||||
|     "complete_the_form": "Answer the questions...", |     "complete_the_form": "Answer the questions...", | ||||||
|     "continue_the_form": "Continue the form", |     "continue_the_form": "Continue the form", | ||||||
|     "feedbackclose": "Allow answers to", |     "feedbackclose": "Allow answers to", | ||||||
|     "feedbackopen": "Allow answers from", |     "feedbackopen": "Allow answers from", | ||||||
|     "feedback_is_not_open": "The feedback is not open", |     "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", |     "mode": "Mode", | ||||||
|  |     "next_page": "Next page", | ||||||
|     "non_anonymous": "User's name will be logged and shown with answers", |     "non_anonymous": "User's name will be logged and shown with answers", | ||||||
|     "non_anonymous_entries": "Non anonymous entries ({{$a}})", |     "non_anonymous_entries": "Non anonymous entries ({{$a}})", | ||||||
|     "non_respondents_students": "Non respondents students ({{$a}})", |     "non_respondents_students": "Non respondents students ({{$a}})", | ||||||
|     "not_selected": "Not selected", |     "not_selected": "Not selected", | ||||||
|     "not_started": "Not started", |     "not_started": "Not started", | ||||||
|  |     "numberoutofrange": "Number out of range", | ||||||
|     "overview": "Overview", |     "overview": "Overview", | ||||||
|     "page_after_submit": "Completion message", |     "page_after_submit": "Completion message", | ||||||
|     "preview": "Preview", |     "preview": "Preview", | ||||||
|  |     "previous_page": "Previous page", | ||||||
|     "questions": "Questions", |     "questions": "Questions", | ||||||
|     "responses": "Responses", |     "responses": "Responses", | ||||||
|     "response_nr": "Response number", |     "response_nr": "Response number", | ||||||
|  |     "save_entries": "Submit your answers", | ||||||
|  |     "show_entries": "Show responses", | ||||||
|     "show_nonrespondents": "Show non-respondents", |     "show_nonrespondents": "Show non-respondents", | ||||||
|     "started": "Started", |     "started": "Started", | ||||||
|     "this_feedback_is_already_submitted": "You've already completed this activity." |     "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 { CoreSitesProvider } from '@providers/sites'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
|  | import { CoreAppProvider } from '@providers/app'; | ||||||
|  | import { AddonModFeedbackOfflineProvider } from './offline'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service that provides some features for feedbacks. |  * Service that provides some features for feedbacks. | ||||||
| @ -35,10 +37,255 @@ export class AddonModFeedbackProvider { | |||||||
|     protected logger; |     protected logger; | ||||||
| 
 | 
 | ||||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, |     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'); |         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. |      * Get analysis information for a given feedback. | ||||||
|      * |      * | ||||||
| @ -436,6 +683,118 @@ export class AddonModFeedbackProvider { | |||||||
|         return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':nonrespondents:'; |         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. |      * Returns the feedback user responses. | ||||||
|      * |      * | ||||||
| @ -723,6 +1082,93 @@ export class AddonModFeedbackProvider { | |||||||
|         return this.sitesProvider.getCurrentSite().write('mod_feedback_view_feedback', params); |         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. |      * 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. |      * Returns the feedback user responses with extra info. | ||||||
|      * |      * | ||||||
| @ -269,8 +351,6 @@ export class AddonModFeedbackHelperProvider { | |||||||
|             parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP) || []; |             parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP) || []; | ||||||
|             item.presentation = parts.length > 0 ? parts[0] : ''; |             item.presentation = parts.length > 0 ? parts[0] : ''; | ||||||
|             // Horizontal are not supported right now. item.horizontal = parts.length > 1 && !!parts[1];
 |             // 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) || []; |         item.choices = item.presentation.split(AddonModFeedbackProvider.LINE_SEP) || []; | ||||||
| @ -320,9 +400,6 @@ export class AddonModFeedbackHelperProvider { | |||||||
|         const data = this.textUtils.parseJSON(item.otherdata); |         const data = this.textUtils.parseJSON(item.otherdata); | ||||||
|         if (data && data.length > 3) { |         if (data && data.length > 3) { | ||||||
|             item.captcha = { |             item.captcha = { | ||||||
|                 challengehash: data[0], |  | ||||||
|                 imageurl: data[1], |  | ||||||
|                 jsurl: data[2], |  | ||||||
|                 recaptchapublickey: data[3] |                 recaptchapublickey: data[3] | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -157,7 +157,7 @@ export class AddonModFeedbackOfflineProvider { | |||||||
|                     timemodified: this.timeUtils.timestamp() |                     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. |      *                           in the filepool root feedback. | ||||||
|      * @return {Promise<any>} Promise resolved when all content is downloaded. Data returned is not reliable. |      * @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 = [], |         const promises = [], | ||||||
|             siteId = this.sitesProvider.getCurrentSiteId(); |             siteId = this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
| @ -119,7 +119,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan | |||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         return Promise.all(promises); |         return Promise.all(promises); | ||||||
|     }*/ |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get the list of downloadable files. |      * 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. |      * @param  {string} [siteId]  Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}     Promise resolved with the list of files. |      * @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 = []; |         let files = []; | ||||||
| 
 | 
 | ||||||
|         return this.feedbackProvider.getFeedback(courseId, module.id).then((feedback) => { |         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.
 |             // Any error, return the list we have.
 | ||||||
|             return files; |             return files; | ||||||
|         }); |         }); | ||||||
|     }*/ |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns feedback intro files. |      * Returns feedback intro files. | ||||||
|  | |||||||
| @ -567,8 +567,17 @@ textarea { | |||||||
|     height: auto; |     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) { | @each $color-name, $color-base, $color-contrast in get-colors($colors) { | ||||||
|  |   // Message cards. | ||||||
|   .core-#{$color-name}-card { |   .core-#{$color-name}-card { | ||||||
|     @extend ion-card; |     @extend ion-card; | ||||||
|     border-bottom: 3px solid $color-base; |     border-bottom: 3px solid $color-base; | ||||||
| @ -589,21 +598,18 @@ textarea { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| canvas[core-chart] { |   .core-#{$color-name}-item { | ||||||
|   max-width: 500px; |     border-bottom: 3px solid $color-base !important; | ||||||
|   margin: 0 auto; |     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 { |   .core-#{$color-name}-circle { | ||||||
|     margin: 0 4px; |     margin: 0 4px; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   .core-#{$color-name}-circle:before { |   .core-#{$color-name}-circle:before { | ||||||
|     @extend .core-circle:before; |     @extend .core-circle:before; | ||||||
|     color: $color-base; |     color: $color-base; | ||||||
|  | |||||||
| @ -59,7 +59,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | |||||||
| 
 | 
 | ||||||
|         // Refresh online status when changes.
 |         // Refresh online status when changes.
 | ||||||
|         this.onlineObserver = network.onchange().subscribe((online) => { |         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. |      * Blocks leaving a view. | ||||||
|  |      * @deprecated, use ionViewCanLeave instead. | ||||||
|      */ |      */ | ||||||
|     blockLeaveView(): void { |     blockLeaveView(): void { | ||||||
|         // @todo
 |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user