forked from EVOgeek/Vmeda.Online
		
	
						commit
						b1a1892be3
					
				| @ -18,7 +18,7 @@ import { ModalController } from 'ionic-angular'; | ||||
| /** | ||||
|  * Base class for component to render a feedback plugin. | ||||
|  */ | ||||
| export class AddonModAssignFeedbackPluginComponent { | ||||
| export class AddonModAssignFeedbackPluginComponentBase { | ||||
|     @Input() assign: any; // The assignment.
 | ||||
|     @Input() submission: any; // The submission.
 | ||||
|     @Input() plugin: any; // The plugin object.
 | ||||
|  | ||||
| @ -76,7 +76,7 @@ | ||||
| 
 | ||||
|         <!-- Ungrouped users. --> | ||||
|         <div *ngIf="assign.teamsubmission && summary && summary.warnofungroupedusers" class="core-info-card" icon-start> | ||||
|             <ion-icon name="information"></ion-icon> | ||||
|             <ion-icon name="information-circle"></ion-icon> | ||||
|             {{ 'addon.mod_assign.ungroupedusers' | translate }} | ||||
|         </div> | ||||
|     </ion-card> | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { AddonModAssignProvider } from '../../../providers/assign'; | ||||
| import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline'; | ||||
| import { AddonModAssignFeedbackDelegate } from '../../../providers/feedback-delegate'; | ||||
| import { AddonModAssignFeedbackPluginComponent } from '../../../classes/feedback-plugin-component'; | ||||
| import { AddonModAssignFeedbackPluginComponentBase } from '../../../classes/feedback-plugin-component'; | ||||
| import { AddonModAssignFeedbackCommentsHandler } from '../providers/handler'; | ||||
| 
 | ||||
| /** | ||||
| @ -30,7 +30,7 @@ import { AddonModAssignFeedbackCommentsHandler } from '../providers/handler'; | ||||
|     selector: 'addon-mod-assign-feedback-comments', | ||||
|     templateUrl: 'comments.html' | ||||
| }) | ||||
| export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedbackPluginComponent implements OnInit { | ||||
| export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedbackPluginComponentBase implements OnInit { | ||||
| 
 | ||||
|     control: FormControl; | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { ModalController } from 'ionic-angular'; | ||||
| import { AddonModAssignProvider } from '../../../providers/assign'; | ||||
| import { AddonModAssignFeedbackPluginComponent } from '../../../classes/feedback-plugin-component'; | ||||
| import { AddonModAssignFeedbackPluginComponentBase } from '../../../classes/feedback-plugin-component'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a edit pdf feedback plugin. | ||||
| @ -24,7 +24,7 @@ import { AddonModAssignFeedbackPluginComponent } from '../../../classes/feedback | ||||
|     selector: 'addon-mod-assign-feedback-edit-pdf', | ||||
|     templateUrl: 'editpdf.html' | ||||
| }) | ||||
| export class AddonModAssignFeedbackEditPdfComponent extends AddonModAssignFeedbackPluginComponent implements OnInit { | ||||
| export class AddonModAssignFeedbackEditPdfComponent extends AddonModAssignFeedbackPluginComponentBase implements OnInit { | ||||
| 
 | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     files: any[]; | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { ModalController } from 'ionic-angular'; | ||||
| import { AddonModAssignProvider } from '../../../providers/assign'; | ||||
| import { AddonModAssignFeedbackPluginComponent } from '../../../classes/feedback-plugin-component'; | ||||
| import { AddonModAssignFeedbackPluginComponentBase } from '../../../classes/feedback-plugin-component'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a file feedback plugin. | ||||
| @ -24,7 +24,7 @@ import { AddonModAssignFeedbackPluginComponent } from '../../../classes/feedback | ||||
|     selector: 'addon-mod-assign-feedback-file', | ||||
|     templateUrl: 'file.html' | ||||
| }) | ||||
| export class AddonModAssignFeedbackFileComponent extends AddonModAssignFeedbackPluginComponent implements OnInit { | ||||
| export class AddonModAssignFeedbackFileComponent extends AddonModAssignFeedbackPluginComponentBase implements OnInit { | ||||
| 
 | ||||
|     component = AddonModAssignProvider.COMPONENT; | ||||
|     files: any[]; | ||||
|  | ||||
| @ -34,7 +34,7 @@ | ||||
| 
 | ||||
|     <!-- Inform what will happen with the choices. --> | ||||
|     <div *ngIf="canEdit && publishInfo && options && options.length" class="core-info-card" icon-start> | ||||
|         <ion-icon name="information"></ion-icon> | ||||
|         <ion-icon name="information-circle"></ion-icon> | ||||
|         {{ publishInfo | translate }} | ||||
|     </div> | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										45
									
								
								src/addon/mod/lesson/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addon/mod/lesson/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| // (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 { CommonModule } from '@angular/common'; | ||||
| import { IonicModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CoreCourseComponentsModule } from '@core/course/components/components.module'; | ||||
| import { AddonModLessonIndexComponent } from './index/index'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModLessonIndexComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         TranslateModule.forChild(), | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CoreCourseComponentsModule | ||||
|     ], | ||||
|     providers: [ | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModLessonIndexComponent | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonModLessonIndexComponent | ||||
|     ] | ||||
| }) | ||||
| export class AddonModLessonComponentsModule {} | ||||
							
								
								
									
										247
									
								
								src/addon/mod/lesson/components/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								src/addon/mod/lesson/components/index/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,247 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons end> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline"  [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
| <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
| 
 | ||||
|     <core-tabs [selectedIndex]="selectedTab"> | ||||
|         <!-- Index/Preview tab. --> | ||||
|         <core-tab [title]="'addon.mod_lesson.preview' | translate"> | ||||
|             <ng-template> | ||||
|                 <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description> | ||||
| 
 | ||||
|                 <!-- Prevent access messages. Only show the first one. --> | ||||
|                 <div class="core-info-card" icon-start *ngIf="lesson && preventMessages && preventMessages.length"> | ||||
|                     <ion-icon name="information-circle"></ion-icon> | ||||
|                     <core-format-text [component]="component" [componentId]="componentId" [text]="preventMessages[0].message"></core-format-text> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <!-- Lesson has data to be synchronized --> | ||||
|                 <div class="core-warning-card" icon-start *ngIf="hasOffline"> | ||||
|                     <ion-icon name="warning"></ion-icon> | ||||
|                     {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} | ||||
|                 </div> | ||||
| 
 | ||||
|                 <!-- Input password for protected lessons. --> | ||||
|                 <ion-card *ngIf="askPassword"> | ||||
|                     <form ion-list (ngSubmit)="submitPassword(passwordinput)"> | ||||
|                         <ion-item> | ||||
|                             <core-show-password item-content [name]="'password'"> | ||||
|                                 <ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> | ||||
|                                 <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"  core-auto-focus #passwordinput></ion-input> | ||||
|                             </core-show-password> | ||||
|                         </ion-item> | ||||
|                         <ion-item> | ||||
|                             <button ion-button block type="submit" icon-end> | ||||
|                                 {{ 'addon.mod_lesson.continue' | translate }} | ||||
|                                 <ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon> | ||||
|                             </button> | ||||
|                         </ion-item> | ||||
|                     </form> | ||||
|                 </ion-card> | ||||
| 
 | ||||
|                 <core-loading [hideUntil]="!showSpinner"> | ||||
|                     <ion-list *ngIf="lesson && (!preventMessages || !preventMessages.length)"> | ||||
|                         <ion-item text-wrap *ngIf="retakeToReview"> | ||||
|                             <!-- A retake was finished in a synchronization, allow reviewing it. --> | ||||
|                             <p>{{ 'addon.mod_lesson.retakefinishedinsync' | translate }}</p> | ||||
|                             <a ion-button block (click)="review()">{{ 'addon.mod_lesson.review' | translate }}</a> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item text-wrap *ngIf="leftDuringTimed && !lesson.timelimit"> | ||||
|                             <!-- User left during the session and there is no time limit, ask to continue. --> | ||||
|                             <p [innerHTML]="'addon.mod_lesson.youhaveseen' | translate"></p> | ||||
|                             <ion-grid> | ||||
|                                 <ion-row> | ||||
|                                     <ion-col> | ||||
|                                         <a ion-button block color="light" (click)="start(false)">{{ 'core.no' | translate }}</a> | ||||
|                                     </ion-col> | ||||
|                                     <ion-col> | ||||
|                                         <a ion-button block (click)="start(true)">{{ 'core.yes' | translate }}</a> | ||||
|                                     </ion-col> | ||||
|                                 </ion-row> | ||||
|                             </ion-grid> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item text-wrap *ngIf="leftDuringTimed && lesson.timelimit && lesson.retake"> | ||||
|                             <!-- User left during the session with time limit and retakes allowed, ask to continue. --> | ||||
|                             <p [innerHTML]="'addon.mod_lesson.leftduringtimed' | translate"></p> | ||||
|                             <a ion-button block icon-end (click)="start(false)"> | ||||
|                                 {{ 'addon.mod_lesson.continue' | translate }} | ||||
|                                 <ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon> | ||||
|                             </a> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item text-wrap *ngIf="leftDuringTimed && lesson.timelimit && !lesson.retake"> | ||||
|                             <!-- User left during the session with time limit and retakes not allowed. This should be handled by preventMessages --> | ||||
|                             <p [innerHTML]="'addon.mod_lesson.leftduringtimednoretake' | translate"></p> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item text-wrap *ngIf="!leftDuringTimed"> | ||||
|                             <!-- User hasn't left during the session, show a start button. --> | ||||
|                             <a ion-button block *ngIf="!canManage" icon-end (click)="start(false)"> | ||||
|                                 {{ 'core.start' | translate }} | ||||
|                                 <ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon> | ||||
|                             </a> | ||||
|                             <a ion-button block *ngIf="canManage" icon-end (click)="start(false)"> | ||||
|                                 {{ 'addon.mod_lesson.preview' | translate }} | ||||
|                                 <ion-icon name="search"></ion-icon> | ||||
|                             </a> | ||||
|                         </ion-item> | ||||
|                     </ion-list> | ||||
|                 </core-loading> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
| 
 | ||||
|         <!-- Reports tab. --> | ||||
|         <core-tab *ngIf="canViewReports" [title]="'addon.mod_lesson.reports' | translate" (ionSelect)="reportsSelected()"> | ||||
|             <ng-template> | ||||
|                 <core-loading [hideUntil]="reportLoaded"> | ||||
|                     <!-- Group selector if the activity uses groups. --> | ||||
|                     <ion-item text-wrap *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)"> | ||||
|                         <ion-label id="addon-mod_lesson-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label> | ||||
|                         <ion-label id="addon-mod_lesson-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label> | ||||
|                         <ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-mod_lesson-groupslabel" interface="popover"> | ||||
|                             <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option> | ||||
|                         </ion-select> | ||||
|                     </ion-item> | ||||
| 
 | ||||
|                     <!-- No lesson retakes. --> | ||||
|                     <core-empty-box *ngIf="!overview && selectedGroupName" icon="stats" [message]="'addon.mod_lesson.nolessonattemptsgroup' | translate:{$a: selectedGroupName}"> | ||||
|                     </core-empty-box> | ||||
|                     <core-empty-box *ngIf="!overview && !selectedGroupName" icon="stats" [message]="'addon.mod_lesson.nolessonattempts' | translate"> | ||||
|                     </core-empty-box> | ||||
| 
 | ||||
|                     <!-- General statistics for the current group. --> | ||||
|                     <ion-card class="addon-mod_lesson-lessonstats" *ngIf="overview"> | ||||
|                         <ion-card-header text-wrap> | ||||
|                             {{ 'addon.mod_lesson.lessonstats' | translate }} | ||||
|                         </ion-card-header> | ||||
| 
 | ||||
|                         <!-- In tablet, max 2 rows with 3 columns. --> | ||||
|                         <ion-list class="hidden-phone"> | ||||
|                             <ion-item text-wrap *ngIf="overview.lessonscored"> | ||||
|                                 <ion-row> | ||||
|                                     <ion-col text-center> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.averagescore' | translate }}</p> | ||||
|                                         <p *ngIf="overview.numofattempts > 0">{{ 'core.percentagenumber' | translate:{$a: overview.avescore} }}</p> | ||||
|                                         <p *ngIf="overview.numofattempts <= 0">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
| 
 | ||||
|                                     <ion-col text-center> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.highscore' | translate }}</p> | ||||
|                                         <p *ngIf="overview.highscore != null">{{ 'core.percentagenumber' | translate:{$a: overview.highscore} }}</p> | ||||
|                                         <p *ngIf="overview.highscore == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
| 
 | ||||
|                                     <ion-col text-center> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.lowscore' | translate }}</p> | ||||
|                                         <p *ngIf="overview.lowscore != null">{{ 'core.percentagenumber' | translate:{$a: overview.lowscore} }}</p> | ||||
|                                         <p *ngIf="overview.lowscore == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
|                                 </ion-row> | ||||
|                             </ion-item> | ||||
| 
 | ||||
|                             <ion-item text-wrap> | ||||
|                                 <ion-row> | ||||
|                                     <ion-col text-center> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.averagetime' | translate }}</p> | ||||
|                                         <p *ngIf="overview.avetime != null && overview.numofattempts">{{ overview.avetimeReadable }}</p> | ||||
|                                         <p *ngIf="overview.avetime == null || !overview.numofattempts">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
| 
 | ||||
|                                     <ion-col text-center> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.hightime' | translate }}</p> | ||||
|                                         <p *ngIf="overview.hightime != null">{{ overview.hightimeReadable }}</p> | ||||
|                                         <p *ngIf="overview.hightime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
| 
 | ||||
|                                     <ion-col text-center> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.lowtime' | translate }}</p> | ||||
|                                         <p *ngIf="overview.lowtime != null">{{ overview.lowtimeReadable }}</p> | ||||
|                                         <p *ngIf="overview.lowtime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
|                                 </ion-row> | ||||
|                             </ion-item> | ||||
|                         </ion-list> | ||||
| 
 | ||||
|                         <!-- In phone, 3 rows with 1 or 2 columns. --> | ||||
|                         <ion-list class="hidden-tablet"> | ||||
|                             <ion-item text-wrap> | ||||
|                                 <ion-row> | ||||
|                                     <ion-col text-center *ngIf="overview.lessonscored"> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.averagescore' | translate }}</p> | ||||
|                                         <p *ngIf="overview.numofattempts > 0">{{ 'core.percentagenumber' | translate:{$a: overview.avescore} }}</p> | ||||
|                                         <p *ngIf="overview.numofattempts <= 0">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
| 
 | ||||
|                                     <ion-col [attr.text-center]="overview.lessonscored ? true : null"> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.averagetime' | translate }}</p> | ||||
|                                         <p *ngIf="overview.avetime != null && overview.numofattempts">{{ overview.avetimeReadable }}</p> | ||||
|                                         <p *ngIf="overview.avetime == null || !overview.numofattempts">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
|                                 </ion-row> | ||||
|                             </ion-item> | ||||
| 
 | ||||
|                             <ion-item text-wrap> | ||||
|                                 <ion-row> | ||||
|                                     <ion-col text-center *ngIf="overview.lessonscored"> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.highscore' | translate }}</p> | ||||
|                                         <p *ngIf="overview.highscore != null">{{ 'core.percentagenumber' | translate:{$a: overview.highscore} }}</p> | ||||
|                                         <p *ngIf="overview.highscore == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
| 
 | ||||
|                                     <ion-col [attr.text-center]="overview.lessonscored ? true : null"> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.hightime' | translate }}</p> | ||||
|                                         <p *ngIf="overview.hightime != null">{{ overview.hightimeReadable }}</p> | ||||
|                                         <p *ngIf="overview.hightime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
|                                 </ion-row> | ||||
|                             </ion-item> | ||||
| 
 | ||||
|                             <ion-item text-wrap> | ||||
|                                 <ion-row> | ||||
|                                     <ion-col text-center *ngIf="overview.lessonscored"> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.lowscore' | translate }}</p> | ||||
|                                         <p *ngIf="overview.lowscore != null">{{ 'core.percentagenumber' | translate:{$a: overview.lowscore} }}</p> | ||||
|                                         <p *ngIf="overview.lowscore == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
| 
 | ||||
|                                     <ion-col [attr.text-center]="overview.lessonscored ? true : null"> | ||||
|                                         <p class="item-heading">{{ 'addon.mod_lesson.lowtime' | translate }}</p> | ||||
|                                         <p *ngIf="overview.lowtime != null">{{ overview.lowtimeReadable }}</p> | ||||
|                                         <p *ngIf="overview.lowtime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                     </ion-col> | ||||
|                                 </ion-row> | ||||
|                             </ion-item> | ||||
|                         </ion-list> | ||||
|                     </ion-card> | ||||
| 
 | ||||
|                     <!-- List of students that have retakes. --> | ||||
|                     <ion-card *ngIf="overview"> | ||||
|                         <ion-card-header text-wrap> | ||||
|                             {{ 'addon.mod_lesson.overview' | translate }} | ||||
|                         </ion-card-header> | ||||
| 
 | ||||
|                         <a ion-item text-wrap *ngFor="let student of overview.students" [navPush]="'AddonModLessonUserRetakePage'" [navParams]="{courseId: courseId, lessonId: lesson.id, userId: student.id}"> | ||||
|                             <ion-avatar item-start *ngIf="student.profileimageurl" core-user-link [userId]="student.id" [courseId]="courseId"> | ||||
|                                 <img [src]="student.profileimageurl" [alt]="'core.pictureof' | translate:{$a: student.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'" role="presentation"> | ||||
|                             </ion-avatar> | ||||
|                             <h2>{{ student.fullname }}</h2> | ||||
|                             <core-progress-bar [progress]="student.bestgrade"></core-progress-bar> | ||||
|                         </a> | ||||
|                     </ion-card> | ||||
|                 </core-loading> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
|     </core-tabs> | ||||
| </core-loading> | ||||
							
								
								
									
										2
									
								
								src/addon/mod/lesson/components/index/index.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/addon/mod/lesson/components/index/index.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| addon-mod-lesson-index { | ||||
| } | ||||
							
								
								
									
										552
									
								
								src/addon/mod/lesson/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										552
									
								
								src/addon/mod/lesson/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,552 @@ | ||||
| // (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, Optional, Injector, Input } from '@angular/core'; | ||||
| import { Content, NavController } from 'ionic-angular'; | ||||
| import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { AddonModLessonProvider } from '../../providers/lesson'; | ||||
| import { AddonModLessonOfflineProvider } from '../../providers/lesson-offline'; | ||||
| import { AddonModLessonSyncProvider } from '../../providers/lesson-sync'; | ||||
| import { AddonModLessonPrefetchHandler } from '../../providers/prefetch-handler'; | ||||
| import { CoreConstants } from '@core/constants'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays a lesson entry page. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-mod-lesson-index', | ||||
|     templateUrl: 'index.html', | ||||
| }) | ||||
| export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityComponent { | ||||
|     @Input() group: number; // The group to display.
 | ||||
|     @Input() action: string; // The "action" to display first.
 | ||||
| 
 | ||||
|     component = AddonModLessonProvider.COMPONENT; | ||||
|     moduleName = 'lesson'; | ||||
| 
 | ||||
|     lesson: any; // The lesson.
 | ||||
|     selectedTab: number; // The initial selected tab.
 | ||||
|     askPassword: boolean; // Whether to ask the password.
 | ||||
|     canManage: boolean; // Whether the user can manage the lesson.
 | ||||
|     canViewReports: boolean; // Whether the user can view the lesson reports.
 | ||||
|     showSpinner: boolean; // Whether to display a spinner.
 | ||||
|     hasOffline: boolean; // Whether there's offline data.
 | ||||
|     retakeToReview: any; // A retake to review.
 | ||||
|     preventMessages: string[]; // List of messages that prevent the lesson from being seen.
 | ||||
|     leftDuringTimed: boolean; // Whether the user has started and left a retake.
 | ||||
|     groupInfo: CoreGroupInfo; // The group info.
 | ||||
|     reportLoaded: boolean; // Whether the report data has been loaded.
 | ||||
|     selectedGroupName: string; // The name of the selected group.
 | ||||
|     overview: any; // Reports overview data.
 | ||||
| 
 | ||||
|     protected syncEventName = AddonModLessonSyncProvider.AUTO_SYNCED; | ||||
|     protected accessInfo: any; // Lesson access info.
 | ||||
|     protected password: string; // The password for the lesson.
 | ||||
|     protected hasPlayed: boolean; // Whether the user has gone to the lesson player (attempted).
 | ||||
| 
 | ||||
|     constructor(injector: Injector, protected lessonProvider: AddonModLessonProvider, @Optional() content: Content, | ||||
|             protected groupsProvider: CoreGroupsProvider, protected lessonOffline: AddonModLessonOfflineProvider, | ||||
|             protected lessonSync: AddonModLessonSyncProvider, protected utils: CoreUtilsProvider, | ||||
|             protected prefetchHandler: AddonModLessonPrefetchHandler, protected navCtrl: NavController, | ||||
|             protected timeUtils: CoreTimeUtilsProvider, protected userProvider: CoreUserProvider) { | ||||
|         super(injector, content); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         super.ngOnInit(); | ||||
| 
 | ||||
|         this.selectedTab = this.action == 'report' ? 1 : 0; | ||||
| 
 | ||||
|         this.loadContent(false, true).then(() => { | ||||
|             if (!this.lesson || (this.preventMessages && this.preventMessages.length)) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.logView(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Change the group displayed. | ||||
|      * | ||||
|      * @param {number} groupId Group ID to display. | ||||
|      */ | ||||
|     changeGroup(groupId: number): void { | ||||
|         this.reportLoaded = false; | ||||
| 
 | ||||
|         this.setGroup(groupId).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting report.'); | ||||
|         }).finally(() => { | ||||
|             this.reportLoaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the lesson data. | ||||
|      * | ||||
|      * @param {boolean} [refresh=false] If it's refreshing content. | ||||
|      * @param {boolean} [sync=false] If the refresh is needs syncing. | ||||
|      * @param {boolean} [showErrors=false] If show errors to the user of hide them. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> { | ||||
| 
 | ||||
|         let lessonReady = true; | ||||
|         this.askPassword = false; | ||||
| 
 | ||||
|         return this.lessonProvider.getLesson(this.courseId, this.module.id).then((lessonData) => { | ||||
|             this.lesson = lessonData; | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.lesson); | ||||
|             this.description = this.lesson.intro; // Show description only if intro is present.
 | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the lesson.
 | ||||
|                 return this.syncActivity(showErrors); | ||||
|             } | ||||
|         }).then(() => { | ||||
|             return this.lessonProvider.getAccessInformation(this.lesson.id); | ||||
|         }).then((info) => { | ||||
|             const promises = []; | ||||
| 
 | ||||
|             this.accessInfo = info; | ||||
|             this.canManage = info.canmanage; | ||||
|             this.canViewReports = info.canviewreports; | ||||
| 
 | ||||
|             if (this.lessonProvider.isLessonOffline(this.lesson)) { | ||||
|                 // Handle status.
 | ||||
|                 this.setStatusListener(); | ||||
| 
 | ||||
|                 // Check if there is offline data.
 | ||||
|                 promises.push(this.lessonSync.hasDataToSync(this.lesson.id, info.attemptscount).then((hasOffline) => { | ||||
|                     this.hasOffline = hasOffline; | ||||
|                 })); | ||||
| 
 | ||||
|                 // Check if there is a retake finished in a synchronization.
 | ||||
|                 promises.push(this.lessonSync.getRetakeFinishedInSync(this.lesson.id).then((retake) => { | ||||
|                     if (retake && retake.retake == info.attemptscount - 1) { | ||||
|                         // The retake finished is still the last retake. Allow reviewing it.
 | ||||
|                         this.retakeToReview = retake; | ||||
|                     } else { | ||||
|                         this.retakeToReview = undefined; | ||||
|                         if (retake) { | ||||
|                             this.lessonSync.deleteRetakeFinishedInSync(this.lesson.id); | ||||
|                         } | ||||
|                     } | ||||
|                 })); | ||||
| 
 | ||||
|                 // Update the list of content pages viewed and question attempts.
 | ||||
|                 promises.push(this.lessonProvider.getContentPagesViewedOnline(this.lesson.id, info.attemptscount)); | ||||
|                 promises.push(this.lessonProvider.getQuestionsAttemptsOnline(this.lesson.id, info.attemptscount)); | ||||
|             } | ||||
| 
 | ||||
|             if (info.preventaccessreasons && info.preventaccessreasons.length) { | ||||
|                 const askPassword = info.preventaccessreasons.length == 1 && this.lessonProvider.isPasswordProtected(info); | ||||
| 
 | ||||
|                 if (askPassword) { | ||||
|                     // The lesson requires a password. Check if there is one in memory or DB.
 | ||||
|                     const promise = this.password ? Promise.resolve(this.password) : | ||||
|                             this.lessonProvider.getStoredPassword(this.lesson.id); | ||||
| 
 | ||||
|                     promises.push(promise.then((password) => { | ||||
|                         return this.validatePassword(password); | ||||
|                     }).catch(() => { | ||||
|                         // No password or the validation failed. Show password form.
 | ||||
|                         this.askPassword = true; | ||||
|                         this.preventMessages = info.preventaccessreasons; | ||||
|                         lessonReady = false; | ||||
|                     })); | ||||
|                 } else  { | ||||
|                     // Lesson cannot be started.
 | ||||
|                     this.preventMessages = info.preventaccessreasons; | ||||
|                     lessonReady = false; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (this.selectedTab == 1 && this.canViewReports) { | ||||
|                 // Only fetch the report data if the tab is selected.
 | ||||
|                 promises.push(this.fetchReportData()); | ||||
|             } | ||||
| 
 | ||||
|             return Promise.all(promises).then(() => { | ||||
|                 if (lessonReady) { | ||||
|                     // Lesson can be started, don't ask the password and don't show prevent messages.
 | ||||
|                     this.lessonReady(refresh); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the reports data. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchReportData(): Promise<any> { | ||||
|         return this.groupsProvider.getActivityGroupInfo(this.module.id).then((groupInfo) => { | ||||
|             this.groupInfo = groupInfo; | ||||
| 
 | ||||
|             return this.setGroup(this.group || 0); | ||||
|         }).finally(() => { | ||||
|             this.reportLoaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if sync has succeed from result sync data. | ||||
|      * | ||||
|      * @param {any} result Data returned on the sync function. | ||||
|      * @return {boolean} If suceed or not. | ||||
|      */ | ||||
|     protected hasSyncSucceed(result: any): boolean { | ||||
|         return result.updated; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User entered the page that contains the component. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         super.ionViewDidEnter(); | ||||
| 
 | ||||
|         // Update data when we come back from the player since the status could have changed.
 | ||||
|         if (this.hasPlayed) { | ||||
|             this.hasPlayed = false; | ||||
| 
 | ||||
|             this.showLoadingAndRefresh(true, false); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User left the page that contains the component. | ||||
|      */ | ||||
|     ionViewDidLeave(): void { | ||||
|         super.ionViewDidLeave(); | ||||
| 
 | ||||
|         if (this.navCtrl.getActive().component.name == 'AddonModLessonPlayerPage') { | ||||
|             this.hasPlayed = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the invalidate content function. | ||||
|      * | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     protected invalidateContent(): Promise<any> { | ||||
|         const promises = []; | ||||
| 
 | ||||
|         promises.push(this.lessonProvider.invalidateLessonData(this.courseId)); | ||||
| 
 | ||||
|         if (this.lesson) { | ||||
|             promises.push(this.lessonProvider.invalidateAccessInformation(this.lesson.id)); | ||||
|             promises.push(this.lessonProvider.invalidatePages(this.lesson.id)); | ||||
|             promises.push(this.lessonProvider.invalidateLessonWithPassword(this.lesson.id)); | ||||
|             promises.push(this.lessonProvider.invalidateTimers(this.lesson.id)); | ||||
|             promises.push(this.lessonProvider.invalidateContentPagesViewed(this.lesson.id)); | ||||
|             promises.push(this.lessonProvider.invalidateQuestionsAttempts(this.lesson.id)); | ||||
|             promises.push(this.lessonProvider.invalidateRetakesOverview(this.lesson.id)); | ||||
|             promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.module.id)); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.all(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Compares sync event data with current data to check if refresh content is needed. | ||||
|      * | ||||
|      * @param {any} syncEventData Data receiven on sync observer. | ||||
|      * @return {boolean} True if refresh is needed, false otherwise. | ||||
|      */ | ||||
|     protected isRefreshSyncNeeded(syncEventData: any): boolean { | ||||
|         return this.lesson && syncEventData.lessonId == this.lesson.id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function called when the lesson is ready to be seen (no pending prevent access reasons). | ||||
|      * | ||||
|      * @param {boolean} [refresh=false] If it's refreshing content. | ||||
|      */ | ||||
|     protected lessonReady(refresh?: boolean): void { | ||||
|         this.askPassword = false; | ||||
|         this.preventMessages = []; | ||||
|         this.leftDuringTimed = this.hasOffline || this.lessonProvider.leftDuringTimed(this.accessInfo); | ||||
| 
 | ||||
|         if (this.password) { | ||||
|             // Store the password in DB.
 | ||||
|             this.lessonProvider.storePassword(this.lesson.id, this.password); | ||||
|         } | ||||
| 
 | ||||
|         // All data obtained, now fill the context menu.
 | ||||
|         this.fillContextMenu(refresh); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Log viewing the lesson. | ||||
|      */ | ||||
|     protected logView(): void { | ||||
|         this.lessonProvider.logViewLesson(this.lesson.id, this.password).then(() => { | ||||
|             this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); | ||||
|         }).catch((error) => { | ||||
|             // Ignore errors.
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open the lesson player. | ||||
|      * | ||||
|      * @param  {boolean} continueLast Whether to continue the last retake. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected playLesson(continueLast: boolean): Promise<any> { | ||||
|         // Calculate the pageId to load. If there is timelimit, lesson is always restarted from the start.
 | ||||
|         let promise; | ||||
| 
 | ||||
|         if (this.hasOffline) { | ||||
|             if (continueLast) { | ||||
|                 promise = this.lessonProvider.getLastPageSeen(this.lesson.id, this.accessInfo.attemptscount); | ||||
|             } else { | ||||
|                 promise = Promise.resolve(this.accessInfo.firstpageid); | ||||
|             } | ||||
|         } else if (this.leftDuringTimed && !this.lesson.timelimit) { | ||||
|             promise = Promise.resolve(continueLast ? this.accessInfo.lastpageseen : this.accessInfo.firstpageid); | ||||
|         } else { | ||||
|             promise = Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         return promise.then((pageId) => { | ||||
|             this.navCtrl.push('AddonModLessonPlayerPage', { | ||||
|                 courseId: this.courseId, | ||||
|                 lessonId: this.lesson.id, | ||||
|                 pageId: pageId, | ||||
|                 password: this.password | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Reports tab selected. | ||||
|      */ | ||||
|     reportsSelected(): void { | ||||
|         if (!this.groupInfo) { | ||||
|             this.fetchReportData().catch((error) => { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'Error getting report.'); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Review the lesson. | ||||
|      */ | ||||
|     review(): void { | ||||
|         if (!this.retakeToReview) { | ||||
|             // No retake to review, stop.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.navCtrl.push('AddonModLessonPlayerPage', { | ||||
|             courseId: this.courseId, | ||||
|             lessonId: this.lesson.id, | ||||
|             pageId: this.retakeToReview.pageId, | ||||
|             password: this.password, | ||||
|             review: true, | ||||
|             retake: this.retakeToReview.retake | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set a group to view the reports. | ||||
|      * | ||||
|      * @param  {number} groupId Group ID. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected setGroup(groupId: number): Promise<any> { | ||||
|         this.group = groupId; | ||||
|         this.selectedGroupName = ''; | ||||
| 
 | ||||
|         // Search the name of the group if it isn't all participants.
 | ||||
|         if (groupId && this.groupInfo && this.groupInfo.groups) { | ||||
|             for (let i = 0; i < this.groupInfo.groups.length; i++) { | ||||
|                 const group = this.groupInfo.groups[i]; | ||||
|                 if (groupId == group.id) { | ||||
|                     this.selectedGroupName = group.name; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Get the overview of retakes for the group.
 | ||||
|         return this.lessonProvider.getRetakesOverview(this.lesson.id, groupId).then((data) => { | ||||
|             const promises = []; | ||||
| 
 | ||||
|             // Format times and grades.
 | ||||
|             if (data && data.avetime != null && data.numofattempts) { | ||||
|                 data.avetime = Math.floor(data.avetime / data.numofattempts); | ||||
|                 data.avetimeReadable = this.timeUtils.formatTime(data.avetime); | ||||
|             } | ||||
| 
 | ||||
|             if (data && data.hightime != null) { | ||||
|                 data.hightimeReadable = this.timeUtils.formatTime(data.hightime); | ||||
|             } | ||||
| 
 | ||||
|             if (data && data.lowtime != null) { | ||||
|                 data.lowtimeReadable = this.timeUtils.formatTime(data.lowtime); | ||||
|             } | ||||
| 
 | ||||
|             if (data && data.lessonscored) { | ||||
|                 if (data.numofattempts) { | ||||
|                     data.avescore = this.textUtils.roundToDecimals(data.avescore, 2); | ||||
|                 } | ||||
|                 if (data.highscore != null) { | ||||
|                     data.highscore = this.textUtils.roundToDecimals(data.highscore, 2); | ||||
|                 } | ||||
|                 if (data.lowscore != null) { | ||||
|                     data.lowscore = this.textUtils.roundToDecimals(data.lowscore, 2); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (data && data.students) { | ||||
|                 // Get the user data for each student returned.
 | ||||
|                 data.students.forEach((student) => { | ||||
|                     student.bestgrade = this.textUtils.roundToDecimals(student.bestgrade, 2); | ||||
| 
 | ||||
|                     promises.push(this.userProvider.getProfile(student.id, this.courseId, true).then((user) => { | ||||
|                         student.profileimageurl = user.profileimageurl; | ||||
|                     }).catch(() => { | ||||
|                         // Error getting profile, resolve promise without adding any extra data.
 | ||||
|                     })); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return this.utils.allPromises(promises).catch(() => { | ||||
|                 // Shouldn't happen.
 | ||||
|             }).then(() => { | ||||
|                 this.overview = data; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Displays some data based on the current status. | ||||
|      * | ||||
|      * @param {string} status The current status. | ||||
|      * @param {string} [previousStatus] The previous status. If not defined, there is no previous status. | ||||
|      */ | ||||
|     protected showStatus(status: string, previousStatus?: string): void { | ||||
|         this.showSpinner = status == CoreConstants.DOWNLOADING; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Start the lesson. | ||||
|      * | ||||
|      * @param {boolean} [continueLast] Whether to continue the last attempt. | ||||
|      */ | ||||
|     start(continueLast?: boolean): void { | ||||
|         if (this.showSpinner) { | ||||
|             // Lesson is being downloaded, abort.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (this.lessonProvider.isLessonOffline(this.lesson)) { | ||||
|             // Lesson supports offline, check if it needs to be downloaded.
 | ||||
|             if (this.currentStatus != CoreConstants.DOWNLOADED) { | ||||
|                 // Prefetch the lesson.
 | ||||
|                 this.showSpinner = true; | ||||
| 
 | ||||
|                 this.prefetchHandler.prefetch(this.module, this.courseId, true).then(() => { | ||||
|                     // Success downloading, open lesson.
 | ||||
|                     this.playLesson(continueLast); | ||||
|                 }).catch((error) => { | ||||
|                     if (this.hasOffline) { | ||||
|                         // Error downloading but there is something offline, allow continuing it.
 | ||||
|                         this.playLesson(continueLast); | ||||
|                     } else { | ||||
|                         this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); | ||||
|                     } | ||||
|                 }).finally(() => { | ||||
|                     this.showSpinner = false; | ||||
|                 }); | ||||
|             } else { | ||||
|                 // Already downloaded, open it.
 | ||||
|                 this.playLesson(continueLast); | ||||
|             } | ||||
|         } else { | ||||
|             this.playLesson(continueLast); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Submit password for password protected lessons. | ||||
|      * | ||||
|      * @param {HTMLInputElement} passwordEl The password input. | ||||
|      */ | ||||
|     submitPassword(passwordEl: HTMLInputElement): void { | ||||
|         const password = passwordEl && passwordEl.value; | ||||
|         if (!password) { | ||||
|             this.domUtils.showErrorModal('addon.mod_lesson.emptypassword', true); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.loaded = false; | ||||
|         this.refreshIcon = 'spinner'; | ||||
|         this.syncIcon = 'spinner'; | ||||
| 
 | ||||
|         this.validatePassword(password).then(() => { | ||||
|             // Password validated.
 | ||||
|             this.lessonReady(false); | ||||
| 
 | ||||
|             // Log view now that we have the password.
 | ||||
|             this.logView(); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModal(error); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|             this.refreshIcon = 'refresh'; | ||||
|             this.syncIcon = 'sync'; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Performs the sync of the activity. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected sync(): Promise<any> { | ||||
|         return this.lessonSync.syncLesson(this.lesson.id, true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Validate a password and retrieve extra data. | ||||
|      * | ||||
|      * @param {string} password The password to validate. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected validatePassword(password: string): Promise<any> { | ||||
|         return this.lessonProvider.getLessonWithPassword(this.lesson.id, password).then((lessonData) => { | ||||
|             this.lesson = lessonData; | ||||
|             this.password = password; | ||||
|         }).catch((error) => { | ||||
|             this.password = ''; | ||||
| 
 | ||||
|             return Promise.reject(error); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										85
									
								
								src/addon/mod/lesson/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/addon/mod/lesson/lang/en.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| { | ||||
|     "answer": "Answer", | ||||
|     "attempt": "Attempt: {{$a}}", | ||||
|     "attemptheader": "Attempt", | ||||
|     "attemptsremaining": "You have {{$a}} attempt(s) remaining", | ||||
|     "averagescore": "Average score", | ||||
|     "averagetime": "Average time", | ||||
|     "branchtable": "Content", | ||||
|     "cannotfindattempt": "Error: could not find attempt", | ||||
|     "cannotfinduser": "Error: could not find users", | ||||
|     "clusterjump": "Unseen question within a cluster", | ||||
|     "completed": "Completed", | ||||
|     "congratulations": "Congratulations - end of lesson reached", | ||||
|     "continue": "Continue", | ||||
|     "continuetonextpage": "Continue to next page.", | ||||
|     "defaultessayresponse": "Your essay will be graded by your teacher.", | ||||
|     "detailedstats": "Detailed statistics", | ||||
|     "didnotanswerquestion": "Did not answer this question.", | ||||
|     "displayofgrade": "Display of grade (for students only)", | ||||
|     "displayscorewithessays": "<p>You earned {{$a.score}} out of {{$a.tempmaxgrade}} for the automatically graded questions.</p><p>Your {{$a.essayquestions}} essay question(s) will be graded and added into your final score at a later date.</p><p>Your current grade without the essay question(s) is {{$a.score}} out of {{$a.grade}}.</p>", | ||||
|     "displayscorewithoutessays": "Your score is {{$a.score}} (out of {{$a.grade}}).", | ||||
|     "emptypassword": "Password cannot be empty", | ||||
|     "enterpassword": "Please enter the password:", | ||||
|     "eolstudentoutoftimenoanswers": "You did not answer any questions.  You have received a 0 for this lesson.", | ||||
|     "errorprefetchrandombranch": "This lesson contains a jump to a random content page. It can't be attempted in the app until it has been started in a web browser.", | ||||
|     "errorreviewretakenotlast": "This attempt can no longer be reviewed because another attempt has been finished.", | ||||
|     "finish": "Finish", | ||||
|     "finishretakeoffline": "This attempt was finished offline.", | ||||
|     "firstwrong": "You have answered incorrectly. Would you like to attempt the question again? (If you now answer the question correctly, it will not count towards your final score.)", | ||||
|     "gotoendoflesson": "Go to the end of the lesson", | ||||
|     "grade": "Grade", | ||||
|     "highscore": "High score", | ||||
|     "hightime": "High time", | ||||
|     "leftduringtimed": "You have left during a timed lesson.<br />Please click on Continue to restart the lesson.", | ||||
|     "leftduringtimednoretake": "You have left during a timed lesson and you are<br />not allowed to retake or continue the lesson.", | ||||
|     "lessonmenu": "Lesson menu", | ||||
|     "lessonstats": "Lesson statistics", | ||||
|     "linkedmedia": "Linked media", | ||||
|     "loginfail": "Login failed, please try again...", | ||||
|     "lowscore": "Low score", | ||||
|     "lowtime": "Low time", | ||||
|     "maximumnumberofattemptsreached": "Maximum number of attempts reached - Moving to next page", | ||||
|     "modattemptsnoteacher": "Student review only works for students.", | ||||
|     "noanswer": "One or more questions have no answer given.  Please go back and submit an answer.", | ||||
|     "nolessonattempts": "No attempts have been made on this lesson.", | ||||
|     "nolessonattemptsgroup": "No attempts have been made by {{$a}} group members on this lesson.", | ||||
|     "notcompleted": "Not completed", | ||||
|     "numberofcorrectanswers": "Number of correct answers: {{$a}}", | ||||
|     "numberofpagesviewed": "Number of questions answered: {{$a}}", | ||||
|     "numberofpagesviewednotice": "Number of questions answered: {{$a.nquestions}} (You should answer at least {{$a.minquestions}})", | ||||
|     "ongoingcustom": "You have earned {{$a.score}} point(s) out of {{$a.currenthigh}} point(s) thus far.", | ||||
|     "ongoingnormal": "You have answered {{$a.correct}} correctly out of {{$a.viewed}} attempts.", | ||||
|     "or": "OR", | ||||
|     "overview": "Overview", | ||||
|     "preview": "Preview", | ||||
|     "progressbarteacherwarning2": "You will not see the progress bar because you can edit this lesson", | ||||
|     "progresscompleted": "You have completed {{$a}}% of the lesson", | ||||
|     "question": "Question", | ||||
|     "rawgrade": "Raw grade", | ||||
|     "reports": "Reports", | ||||
|     "response": "Response", | ||||
|     "retakefinishedinsync": "An offline attempt was synchronised. Do you want to review it?", | ||||
|     "retakelabelfull": "{{retake}}: {{grade}} {{timestart}} ({{duration}})", | ||||
|     "retakelabelshort": "{{retake}}: {{grade}} {{timestart}}", | ||||
|     "review": "Review", | ||||
|     "reviewlesson": "Review lesson", | ||||
|     "reviewquestionback": "Yes, I'd like to try again", | ||||
|     "reviewquestioncontinue": "No, I just want to go on to the next question", | ||||
|     "secondpluswrong": "Not quite.  Would you like to try again?", | ||||
|     "submit": "Submit", | ||||
|     "teacherjumpwarning": "An {{$a.cluster}} jump or an {{$a.unseen}} jump is being used in this lesson.  The next page jump will be used instead.  Login as a student to test these jumps.", | ||||
|     "teacherongoingwarning": "Ongoing score is only displayed for student.  Login as a student to test ongoing score", | ||||
|     "teachertimerwarning": "Timer only works for students.  Test the timer by logging in as a student.", | ||||
|     "thatsthecorrectanswer": "That's the correct answer", | ||||
|     "thatsthewronganswer": "That's the wrong answer", | ||||
|     "timeremaining": "Time remaining", | ||||
|     "timetaken": "Time taken", | ||||
|     "unseenpageinbranch": "Unseen question within a content page", | ||||
|     "warningretakefinished": "The attempt was finished on the site.", | ||||
|     "welldone": "Well done!", | ||||
|     "youhaveseen": "You have seen more than one page of this lesson already.<br />Do you want to start at the last page you saw?", | ||||
|     "youranswer": "Your answer", | ||||
|     "yourcurrentgradeisoutof": "Your current grade is {{$a.grade}} out of {{$a.total}}", | ||||
|     "youshouldview": "You should answer at least: {{$a}}" | ||||
| } | ||||
							
								
								
									
										65
									
								
								src/addon/mod/lesson/lesson.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/addon/mod/lesson/lesson.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| // (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 { CoreCronDelegate } from '@providers/cron'; | ||||
| import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; | ||||
| import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; | ||||
| import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; | ||||
| import { AddonModLessonComponentsModule } from './components/components.module'; | ||||
| import { AddonModLessonProvider } from './providers/lesson'; | ||||
| import { AddonModLessonOfflineProvider } from './providers/lesson-offline'; | ||||
| import { AddonModLessonSyncProvider } from './providers/lesson-sync'; | ||||
| import { AddonModLessonHelperProvider } from './providers/helper'; | ||||
| import { AddonModLessonModuleHandler } from './providers/module-handler'; | ||||
| import { AddonModLessonPrefetchHandler } from './providers/prefetch-handler'; | ||||
| import { AddonModLessonSyncCronHandler } from './providers/sync-cron-handler'; | ||||
| import { AddonModLessonIndexLinkHandler } from './providers/index-link-handler'; | ||||
| import { AddonModLessonGradeLinkHandler } from './providers/grade-link-handler'; | ||||
| import { AddonModLessonReportLinkHandler } from './providers/report-link-handler'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|     ], | ||||
|     imports: [ | ||||
|         AddonModLessonComponentsModule | ||||
|     ], | ||||
|     providers: [ | ||||
|         AddonModLessonProvider, | ||||
|         AddonModLessonOfflineProvider, | ||||
|         AddonModLessonSyncProvider, | ||||
|         AddonModLessonHelperProvider, | ||||
|         AddonModLessonModuleHandler, | ||||
|         AddonModLessonPrefetchHandler, | ||||
|         AddonModLessonSyncCronHandler, | ||||
|         AddonModLessonIndexLinkHandler, | ||||
|         AddonModLessonGradeLinkHandler, | ||||
|         AddonModLessonReportLinkHandler | ||||
|     ] | ||||
| }) | ||||
| export class AddonModLessonModule { | ||||
|     constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModLessonModuleHandler, | ||||
|             prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModLessonPrefetchHandler, | ||||
|             cronDelegate: CoreCronDelegate, syncHandler: AddonModLessonSyncCronHandler, linksDelegate: CoreContentLinksDelegate, | ||||
|             indexHandler: AddonModLessonIndexLinkHandler, gradeHandler: AddonModLessonGradeLinkHandler, | ||||
|             reportHandler: AddonModLessonReportLinkHandler) { | ||||
| 
 | ||||
|         moduleDelegate.registerHandler(moduleHandler); | ||||
|         prefetchDelegate.registerHandler(prefetchHandler); | ||||
|         cronDelegate.register(syncHandler); | ||||
|         linksDelegate.registerHandler(indexHandler); | ||||
|         linksDelegate.registerHandler(gradeHandler); | ||||
|         linksDelegate.registerHandler(reportHandler); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/addon/mod/lesson/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/addon/mod/lesson/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar> | ||||
|         <ion-title><core-format-text [text]="title"></core-format-text></ion-title> | ||||
| 
 | ||||
|         <ion-buttons end> | ||||
|             <!-- The buttons defined by the component will be added in here. --> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher [enabled]="lessonComponent.loaded" (ionRefresh)="lessonComponent.doRefresh($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
| 
 | ||||
|     <addon-mod-lesson-index [module]="module" [courseId]="courseId" [group]="group" [action]="action" (dataRetrieved)="updateData($event)"></addon-mod-lesson-index> | ||||
| </ion-content> | ||||
							
								
								
									
										33
									
								
								src/addon/mod/lesson/pages/index/index.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/addon/mod/lesson/pages/index/index.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| // (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 { AddonModLessonComponentsModule } from '../../components/components.module'; | ||||
| import { AddonModLessonIndexPage } from './index'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModLessonIndexPage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreDirectivesModule, | ||||
|         AddonModLessonComponentsModule, | ||||
|         IonicPageModule.forChild(AddonModLessonIndexPage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class AddonModLessonIndexPageModule {} | ||||
							
								
								
									
										66
									
								
								src/addon/mod/lesson/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/addon/mod/lesson/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| // (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, ViewChild } from '@angular/core'; | ||||
| import { IonicPage, NavParams } from 'ionic-angular'; | ||||
| import { AddonModLessonIndexComponent } from '../../components/index/index'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the lesson entry page. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-lesson-index' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-lesson-index', | ||||
|     templateUrl: 'index.html', | ||||
| }) | ||||
| export class AddonModLessonIndexPage { | ||||
|     @ViewChild(AddonModLessonIndexComponent) lessonComponent: AddonModLessonIndexComponent; | ||||
| 
 | ||||
|     title: string; | ||||
|     module: any; | ||||
|     courseId: number; | ||||
|     group: number; // The group to display.
 | ||||
|     action: string; // The "action" to display first.
 | ||||
| 
 | ||||
|     constructor(navParams: NavParams) { | ||||
|         this.module = navParams.get('module') || {}; | ||||
|         this.courseId = navParams.get('courseId'); | ||||
|         this.group = navParams.get('group'); | ||||
|         this.action = navParams.get('action'); | ||||
|         this.title = this.module.name; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update some data based on the lesson instance. | ||||
|      * | ||||
|      * @param {any} lesson Lesson instance. | ||||
|      */ | ||||
|     updateData(lesson: any): void { | ||||
|         this.title = lesson.name || this.title; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User entered the page. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         this.lessonComponent.ionViewDidEnter(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User left the page. | ||||
|      */ | ||||
|     ionViewDidLeave(): void { | ||||
|         this.lessonComponent.ionViewDidLeave(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/addon/mod/lesson/pages/menu-modal/menu-modal.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/addon/mod/lesson/pages/menu-modal/menu-modal.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar> | ||||
|         <ion-title>{{ pageInstance.lesson.name }}</ion-title> | ||||
|         <ion-buttons end> | ||||
|             <button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon name="close"></ion-icon> | ||||
|             </button> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content class="addon-mod_lesson-menu-modal"> | ||||
|     <nav> | ||||
|         <ion-list> | ||||
|             <!-- Media file. --> | ||||
|             <ng-container *ngIf="pageInstance.mediaFile"> | ||||
|                 <ion-item-divider color="light"><h2>{{ 'addon.mod_lesson.linkedmedia' | translate }}</h2></ion-item-divider> | ||||
|                 <core-file [file]="pageInstance.mediaFile" [component]="pageInstance.component" [componentId]="pageInstance.lesson.coursemodule"></core-file> | ||||
|             </ng-container> | ||||
| 
 | ||||
|             <!-- Lesson menu. --> | ||||
|             <ng-container *ngIf="pageInstance.displayMenu"> | ||||
|                 <ion-item-divider color="light"><h2>{{ 'addon.mod_lesson.lessonmenu' | translate }}</h2></ion-item-divider> | ||||
|                 <ion-item text-center *ngIf="pageInstance.loadingMenu"> | ||||
|                     <ion-spinner></ion-spinner> | ||||
|                 </ion-item> | ||||
|                 <div *ngIf="!pageInstance.loadingMenu"> | ||||
|                     <ng-container *ngFor="let page of pageInstance.lessonPages"> | ||||
|                         <a ion-item text-wrap *ngIf="page.display && page.displayinmenublock" (click)="loadPage(page.id)" [ngClass]='{"addon-mod_lesson-selected core-white-push-arrow": !pageInstance.eolData && pageInstance.currentPage == page.id}'> | ||||
|                             <p><core-format-text [text]="page.title"></core-format-text></p> | ||||
|                         </a> | ||||
|                     </ng-container> | ||||
|                 </div> | ||||
|             </ng-container> | ||||
|         </ion-list> | ||||
|     </nav> | ||||
| </ion-content> | ||||
							
								
								
									
										33
									
								
								src/addon/mod/lesson/pages/menu-modal/menu-modal.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/addon/mod/lesson/pages/menu-modal/menu-modal.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| // (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 { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { AddonModLessonMenuModalPage } from './menu-modal'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModLessonMenuModalPage | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         IonicPageModule.forChild(AddonModLessonMenuModalPage), | ||||
|         TranslateModule.forChild() | ||||
|     ] | ||||
| }) | ||||
| export class AddonModLessonMenuModalPageModule {} | ||||
							
								
								
									
										5
									
								
								src/addon/mod/lesson/pages/menu-modal/menu-modal.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/addon/mod/lesson/pages/menu-modal/menu-modal.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| page-addon-mod-lesson-menu-modal { | ||||
|     .addon-mod_lesson-selected, .item.addon-mod_lesson-selected { | ||||
|         background: $blue-light; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										58
									
								
								src/addon/mod/lesson/pages/menu-modal/menu-modal.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/addon/mod/lesson/pages/menu-modal/menu-modal.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| // (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 } from '@angular/core'; | ||||
| import { IonicPage, ViewController, NavParams } from 'ionic-angular'; | ||||
| 
 | ||||
| /** | ||||
|  * Modal that renders the lesson menu and media file. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-lesson-menu-modal' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-lesson-menu-modal', | ||||
|     templateUrl: 'menu-modal.html', | ||||
| }) | ||||
| export class AddonModLessonMenuModalPage { | ||||
| 
 | ||||
|     /** | ||||
|      * The instance of the page that opened the modal. We use the instance instead of the needed attributes for these reasons: | ||||
|      *     - We want the user to be able to see the media file while the menu is being loaded, so we need to be able to update | ||||
|      *       the menu dynamically based on the data retrieved by the page that opened the modal. | ||||
|      *     - The onDidDismiss function takes a while to be called, making the app seem slow. This way we can directly call | ||||
|      *       the functions we need without having to wait for the modal to be dismissed. | ||||
|      * @type {any} | ||||
|      */ | ||||
|     pageInstance: any; | ||||
| 
 | ||||
|     constructor(params: NavParams, protected viewCtrl: ViewController) { | ||||
|         this.pageInstance = params.get('page'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         this.viewCtrl.dismiss(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load a certain page. | ||||
|      * | ||||
|      * @param {number} pageId The page ID to load. | ||||
|      */ | ||||
|     loadPage(pageId: number): void { | ||||
|         this.pageInstance.changePage && this.pageInstance.changePage(pageId); | ||||
|         this.closeModal(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,26 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar> | ||||
|         <ion-title>{{ 'core.login.password' | translate }}</ion-title> | ||||
|         <ion-buttons end> | ||||
|             <button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon name="close"></ion-icon> | ||||
|             </button> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content padding class="addon-mod_lesson-password-modal"> | ||||
|     <form ion-list (ngSubmit)="submitPassword(passwordinput)"> | ||||
|         <ion-item> | ||||
|             <core-show-password item-content [name]="'password'"> | ||||
|                 <ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> | ||||
|                 <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" core-auto-focus #passwordinput></ion-input> | ||||
|             </core-show-password> | ||||
|         </ion-item> | ||||
|         <ion-item> | ||||
|             <button ion-button block type="submit" icon-end> | ||||
|                 {{ 'addon.mod_lesson.continue' | translate }} | ||||
|                 <ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon> | ||||
|             </button> | ||||
|         </ion-item> | ||||
|     </form> | ||||
| </ion-content> | ||||
| @ -0,0 +1,31 @@ | ||||
| // (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 { AddonModLessonPasswordModalPage } from './password-modal'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModLessonPasswordModalPage | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreComponentsModule, | ||||
|         IonicPageModule.forChild(AddonModLessonPasswordModalPage), | ||||
|         TranslateModule.forChild() | ||||
|     ] | ||||
| }) | ||||
| export class AddonModLessonPasswordModalPageModule {} | ||||
							
								
								
									
										43
									
								
								src/addon/mod/lesson/pages/password-modal/password-modal.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/addon/mod/lesson/pages/password-modal/password-modal.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| // (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 } from '@angular/core'; | ||||
| import { IonicPage, ViewController } from 'ionic-angular'; | ||||
| 
 | ||||
| /** | ||||
|  * Modal that asks the password for a lesson. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-lesson-password-modal' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-lesson-password-modal', | ||||
|     templateUrl: 'password-modal.html', | ||||
| }) | ||||
| export class AddonModLessonPasswordModalPage { | ||||
| 
 | ||||
|     constructor(protected viewCtrl: ViewController) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Send the password back. | ||||
|      */ | ||||
|     submitPassword(password: HTMLInputElement): void { | ||||
|         this.viewCtrl.dismiss(password.value); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         this.viewCtrl.dismiss(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										220
									
								
								src/addon/mod/lesson/pages/player/player.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								src/addon/mod/lesson/pages/player/player.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,220 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar> | ||||
|         <ion-title><core-format-text [text]="title"></core-format-text></ion-title> | ||||
| 
 | ||||
|         <ion-buttons end> | ||||
|             <button *ngIf="displayMenu || mediaFile" ion-button icon-only [attr.aria-label]="'addon.mod_lesson.lessonmenu' | translate" (click)="menuModal.present()"> | ||||
|                 <ion-icon name="bookmark"></ion-icon> | ||||
|             </button> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <!-- Info messages. Only show the first one. --> | ||||
|         <div class="core-info-card" icon-start *ngIf="lesson && messages && messages.length"> | ||||
|             <ion-icon name="information-circle"></ion-icon> | ||||
|             <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="messages[0].message"></core-format-text> | ||||
|         </div> | ||||
| 
 | ||||
|         <div *ngIf="lesson" [ngClass]='{"addon-mod_lesson-slideshow": lesson.slideshow}' [ngStyle]="{'width': lessonWidth, 'height': lessonHeight}"> | ||||
| 
 | ||||
|             <core-timer *ngIf="endTime" [endTime]="endTime" (finished)="timeUp()" [timerText]="'addon.mod_lesson.timeremaining' | translate"></core-timer> | ||||
| 
 | ||||
|             <!-- Retake and ongoing score. --> | ||||
|             <ion-item text-wrap *ngIf="showRetake && !eolData && !processData"> | ||||
|                 <p>{{ 'addon.mod_lesson.attempt' | translate:{$a: retake} }}</p> | ||||
|             </ion-item> | ||||
|             <ion-item text-wrap *ngIf="pageData && pageData.ongoingscore && !eolData && !processData" class="addon-mod_lesson-ongoingscore"> | ||||
|                 <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageData.ongoingscore"></core-format-text> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <!-- Page content. --> | ||||
|             <ion-card *ngIf="!eolData && !processData"> | ||||
|                 <!-- Content page. --> | ||||
|                 <ion-item text-wrap *ngIf="!question"> | ||||
|                     <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageContent"></core-format-text> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Question page. --> | ||||
|                 <form *ngIf="question" ion-list [formGroup]="questionForm"> | ||||
|                     <ion-item-divider text-wrap color="light"> | ||||
|                         <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageContent"></core-format-text> | ||||
|                     </ion-item-divider> | ||||
| 
 | ||||
|                     <input *ngFor="let input of question.hiddenInputs" type="hidden" [name]="input.name" [value]="input.value" /> | ||||
| 
 | ||||
|                     <!-- Render a different input depending on the type of the question. --> | ||||
|                     <ng-container [ngSwitch]="question.template"> | ||||
| 
 | ||||
|                         <!-- Short answer. --> | ||||
|                         <ng-container *ngSwitchCase="'shortanswer'"> | ||||
|                             <ion-input padding-left [type]="question.input.type" placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}" [id]="question.input.id" [formControlName]="question.input.name" autocorrect="off" [maxlength]="question.input.maxlength"> | ||||
|                             </ion-input> | ||||
|                         </ng-container> | ||||
| 
 | ||||
|                         <!-- Essay. --> | ||||
|                         <ng-container *ngSwitchCase="'essay'"> | ||||
|                             <ion-item *ngIf="question.textarea"> | ||||
|                                 <core-rich-text-editor item-content placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}" [control]="question.control" [component]="component" [componentId]="lesson.coursemodule"></core-rich-text-editor> | ||||
|                             </ion-item> | ||||
|                             <ion-item text-wrap *ngIf="!question.textarea && question.useranswer"> | ||||
|                                 <p class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</p> | ||||
|                                 <p><core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="question.useranswer"></core-format-text></p> | ||||
|                             </ion-item> | ||||
|                         </ng-container> | ||||
| 
 | ||||
|                         <!-- Multichoice. --> | ||||
|                         <ng-container *ngSwitchCase="'multichoice'"> | ||||
|                             <!-- Single choice. --> | ||||
|                             <div *ngIf="!question.multi" radio-group [formControlName]="question.controlName"> | ||||
|                                 <ion-item text-wrap *ngFor="let option of question.options"> | ||||
|                                     <ion-label> | ||||
|                                         <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="option.text"></core-format-text> | ||||
|                                     </ion-label> | ||||
|                                     <ion-radio [id]="option.id" [value]="option.value" [disabled]="option.disabled"></ion-radio> | ||||
|                                 </ion-item> | ||||
|                             </div> | ||||
| 
 | ||||
|                             <!-- Multiple choice. --> | ||||
|                             <ng-container *ngIf="question.multi"> | ||||
|                                 <ion-item text-wrap *ngFor="let option of question.options"> | ||||
|                                     <ion-label> | ||||
|                                         <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="option.text"></core-format-text> | ||||
|                                     </ion-label> | ||||
|                                     <ion-checkbox [id]="option.id" [formControlName]="option.name" item-end></ion-checkbox> | ||||
|                                 </ion-item> | ||||
|                             </ng-container> | ||||
|                         </ng-container> | ||||
| 
 | ||||
|                         <!-- Matching. --> | ||||
|                         <ng-container *ngSwitchCase="'matching'"> | ||||
|                             <ion-item text-wrap *ngFor="let row of question.rows"> | ||||
|                                 <ion-grid item-content> | ||||
|                                     <ion-row> | ||||
|                                         <ion-col> | ||||
|                                             <p><core-format-text id="addon-mod_lesson-matching-{{row.id}}" [component]="component" [componentId]="lesson.coursemodule" [text]="row.text"></core-format-text></p> | ||||
|                                         </ion-col> | ||||
|                                         <ion-col> | ||||
|                                             <ion-select [id]="row.id" [formControlName]="row.name" [attr.aria-labelledby]="'addon-mod_lesson-matching-' + row.id" interface="popover"> | ||||
|                                                 <ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option> | ||||
|                                             </ion-select> | ||||
|                                         </ion-col> | ||||
|                                     </ion-row> | ||||
|                                 </ion-grid> | ||||
|                             </ion-item> | ||||
|                         </ng-container> | ||||
|                     </ng-container> | ||||
| 
 | ||||
|                     <ion-item> | ||||
|                         <button ion-button block (click)="submitQuestion()">{{ question.submitLabel }}</button> | ||||
|                     </ion-item> | ||||
|                 </form> | ||||
|             </ion-card> | ||||
| 
 | ||||
|             <!-- Page buttons and progress. --> | ||||
|             <ion-list *ngIf="!eolData && !processData"> | ||||
|                 <ion-grid text-wrap *ngIf="pageButtons && pageButtons.length" class="addon-mod_lesson-pagebuttons"> | ||||
|                     <ion-row align-items-center> | ||||
|                         <ion-col *ngFor="let button of pageButtons" col-12 col-md-6 col-lg-3 col-xl> | ||||
|                             <a ion-button block outline text-wrap [id]="button.id" (click)="buttonClicked(button.data)">{{ button.content }}</a> | ||||
|                         </ion-col> | ||||
|                     </ion-row> | ||||
|                 </ion-grid> | ||||
|                 <ion-item text-wrap *ngIf="lesson && lesson.progressbar && !canManage && pageData"> | ||||
|                     <p>{{ 'addon.mod_lesson.progresscompleted' | translate:{$a: pageData.progress} }}</p> | ||||
|                     <core-progress-bar [progress]="pageData.progress"></core-progress-bar> | ||||
|                 </ion-item> | ||||
|                 <div class="core-info-card" icon-start *ngIf="lesson && lesson.progressbar && canManage"> | ||||
|                     <ion-icon name="information-circle"></ion-icon> | ||||
|                     {{ 'addon.mod_lesson.progressbarteacherwarning2' | translate }} | ||||
|                 </div> | ||||
|             </ion-list> | ||||
| 
 | ||||
|             <!-- End of lesson reached. --> | ||||
|             <ion-card *ngIf="eolData && !processData"> | ||||
|                 <div class="core-warning-card" icon-start *ngIf="eolData.offline && eolData.offline.value"> | ||||
|                     <ion-icon name="warning"></ion-icon> | ||||
|                     {{ 'addon.mod_lesson.finishretakeoffline' | translate }} | ||||
|                 </div> | ||||
| 
 | ||||
|                 <h3 padding *ngIf="eolData.gradelesson">{{ 'addon.mod_lesson.congratulations' | translate }}</h3> | ||||
|                 <ion-item text-wrap *ngIf="eolData.notenoughtimespent"> | ||||
|                     <core-format-text [text]="eolData.notenoughtimespent.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.numberofpagesviewed"> | ||||
|                     <core-format-text [text]="eolData.numberofpagesviewed.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.youshouldview"> | ||||
|                     <core-format-text [text]="eolData.youshouldview.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.numberofcorrectanswers"> | ||||
|                     <core-format-text [text]="eolData.numberofcorrectanswers.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.displayscorewithessays"> | ||||
|                     <core-format-text [text]="eolData.displayscorewithessays.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="!eolData.displayscorewithessays && eolData.displayscorewithoutessays"> | ||||
|                     <core-format-text [text]="eolData.displayscorewithoutessays.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.yourcurrentgradeisoutof"> | ||||
|                     <core-format-text [text]="eolData.yourcurrentgradeisoutof.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.eolstudentoutoftimenoanswers"> | ||||
|                     <core-format-text [text]="eolData.eolstudentoutoftimenoanswers.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.welldone"> | ||||
|                     <core-format-text [text]="eolData.welldone.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="lesson.progressbar && eolData.progresscompleted"> | ||||
|                     <p>{{ 'addon.mod_lesson.progresscompleted' | translate:{$a: eolData.progresscompleted.value} }}</p> | ||||
|                     <core-progress-bar [progress]="eolData.progresscompleted.value"></core-progress-bar> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.displayofgrade"> | ||||
|                     <core-format-text [text]="eolData.displayofgrade.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.reviewlesson"> | ||||
|                     <a ion-button block (click)="reviewLesson(eolData.reviewlesson.pageid)"> | ||||
|                         <core-format-text [text]="'addon.mod_lesson.reviewlesson' | translate"></core-format-text> | ||||
|                     </a> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.modattemptsnoteacher"> | ||||
|                     <core-format-text [text]="eolData.modattemptsnoteacher.message"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="eolData.activitylink && eolData.activitylink.value"> | ||||
|                     <ng-container *ngIf="eolData.activitylink.value.formatted"> | ||||
|                         <!-- Activity link was successfully formatted, render the button. --> | ||||
|                         <a ion-button block color="light" [href]="eolData.activitylink.value.href" core-link [capture]="true"> | ||||
|                             <core-format-text [text]="eolData.activitylink.value.label"></core-format-text> | ||||
|                         </a> | ||||
|                     </ng-container> | ||||
|                     <ng-container *ngIf="!eolData.activitylink.value.formatted"> | ||||
|                         <!-- Activity link wasn't formatted, render the original link. --> | ||||
|                         <core-format-text [text]="eolData.activitylink.value.label"></core-format-text> | ||||
|                     </ng-container> | ||||
|                 </ion-item> | ||||
|             </ion-card> | ||||
| 
 | ||||
|             <!-- Feedback returned when processing an action. --> | ||||
|             <ion-list *ngIf="processData"> | ||||
|                 <ion-item text-wrap *ngIf="processData.ongoingscore && !processData.reviewmode" > | ||||
|                     <core-format-text class="addon-mod_lesson-ongoingscore" [text]="processData.ongoingscore"></core-format-text> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="!processData.reviewmode || review"> | ||||
|                     <p *ngIf="!processData.reviewmode"> | ||||
|                         <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="processData.feedback"></core-format-text> | ||||
|                     </p> | ||||
|                     <div *ngIf="review"> | ||||
|                         <p>{{ 'addon.mod_lesson.gotoendoflesson' | translate }}</p> | ||||
|                         <p>{{ 'addon.mod_lesson.or' | translate }}</p> | ||||
|                         <p>{{ 'addon.mod_lesson.continuetonextpage' | translate }}</p> | ||||
|                     </div> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap *ngIf="review || (processData.buttons && processData.buttons.length)"> | ||||
|                     <a ion-button block color="light" *ngIf="review" (click)="changePage(LESSON_EOL)">{{ 'addon.mod_lesson.finish' | translate }}</a> | ||||
|                     <a ion-button block color="light" *ngFor="let button of processData.buttons" (click)="changePage(button.pageId, true)">{{ button.label | translate }}</a> | ||||
|                 </ion-item> | ||||
|             </ion-list> | ||||
|         </div> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										33
									
								
								src/addon/mod/lesson/pages/player/player.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/addon/mod/lesson/pages/player/player.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| // (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 { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { AddonModLessonPlayerPage } from './player'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModLessonPlayerPage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         IonicPageModule.forChild(AddonModLessonPlayerPage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class AddonModLessonPlayerPageModule {} | ||||
							
								
								
									
										21
									
								
								src/addon/mod/lesson/pages/player/player.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/addon/mod/lesson/pages/player/player.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| page-addon-mod-lesson-player { | ||||
|     .addon-mod_lesson-slideshow { | ||||
|         max-width: 100%; | ||||
|         max-height: 100%; | ||||
|         margin: 0 auto; | ||||
|     } | ||||
| 
 | ||||
|     ion-input[padding-left] input[padding-left] { | ||||
|         // Applying padding-left to the ion-input applies it twice since it's replicated in the inner input. | ||||
|         padding-left: 0; | ||||
|     } | ||||
| 
 | ||||
|     .addon-mod_lesson-pagebuttons .button-block { | ||||
|         contain: content; | ||||
|         height: auto; | ||||
| 
 | ||||
|         .button-inner { | ||||
|             height: auto; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										635
									
								
								src/addon/mod/lesson/pages/player/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										635
									
								
								src/addon/mod/lesson/pages/player/player.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,635 @@ | ||||
| // (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, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; | ||||
| import { FormBuilder, FormGroup } from '@angular/forms'; | ||||
| import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSyncProvider } from '@providers/sync'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreUrlUtilsProvider } from '@providers/utils/url'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { AddonModLessonProvider } from '../../providers/lesson'; | ||||
| import { AddonModLessonOfflineProvider } from '../../providers/lesson-offline'; | ||||
| import { AddonModLessonSyncProvider } from '../../providers/lesson-sync'; | ||||
| import { AddonModLessonHelperProvider } from '../../providers/helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that allows attempting and reviewing a lesson. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-lesson-player' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-lesson-player', | ||||
|     templateUrl: 'player.html', | ||||
| }) | ||||
| export class AddonModLessonPlayerPage implements OnInit, OnDestroy { | ||||
|     @ViewChild(Content) content: Content; | ||||
| 
 | ||||
|     component = AddonModLessonProvider.COMPONENT; | ||||
|     LESSON_EOL = AddonModLessonProvider.LESSON_EOL; | ||||
|     questionForm: FormGroup; // The FormGroup for question pages.
 | ||||
|     title: string; // The page title.
 | ||||
|     lesson: any; // The lesson object.
 | ||||
|     currentPage: number; // Current page being viewed.
 | ||||
|     review: boolean; // Whether the user is reviewing.
 | ||||
|     messages: any[]; // Messages to display to the user.
 | ||||
|     menuModal: Modal; // Modal to navigate through the pages.
 | ||||
|     canManage: boolean; // Whether the user can manage the lesson.
 | ||||
|     retake: number; // Current retake number.
 | ||||
|     showRetake: boolean; // Whether the retake number needs to be displayed.
 | ||||
|     lessonWidth: string; // Width of the lesson (if slideshow mode).
 | ||||
|     lessonHeight: string; // Height of the lesson (if slideshow mode).
 | ||||
|     endTime: number; // End time of the lesson if it's timed.
 | ||||
|     pageData: any; // Current page data.
 | ||||
|     pageContent: string; // Current page contents.
 | ||||
|     pageButtons: any[]; // List of buttons of the current page.
 | ||||
|     question: any; // Question of the current page (if it's a question page).
 | ||||
|     eolData: any; // Data for EOL page (if current page is EOL).
 | ||||
|     processData: any; // Data to display after processing a page.
 | ||||
|     loaded: boolean; // Whether data has been loaded.
 | ||||
|     displayMenu: boolean; // Whether the lesson menu should be displayed.
 | ||||
|     originalData: any; // Original question data. It is used to check if data has changed.
 | ||||
| 
 | ||||
|     protected courseId: number; // The course ID the lesson belongs to.
 | ||||
|     protected lessonId: number; // Lesson ID.
 | ||||
|     protected password: string; // Lesson password (if any).
 | ||||
|     protected forceLeave = false; // If true, don't perform any check when leaving the view.
 | ||||
|     protected offline: boolean; // Whether we are in offline mode.
 | ||||
|     protected accessInfo: any; // Lesson access info.
 | ||||
|     protected jumps: any; // All possible jumps.
 | ||||
|     protected mediaFile: any; // Media file of the lesson.
 | ||||
|     protected firstPageLoaded: boolean; // Whether the first page has been loaded.
 | ||||
|     protected loadingMenu: boolean; // Whether the lesson menu is being loaded.
 | ||||
|     protected lessonPages: any[]; // Lesson pages (for the lesson menu).
 | ||||
| 
 | ||||
|     constructor(protected navParams: NavParams, logger: CoreLoggerProvider, protected translate: TranslateService, | ||||
|             protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider, | ||||
|             protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController, | ||||
|             protected timeUtils: CoreTimeUtilsProvider, protected lessonProvider: AddonModLessonProvider, | ||||
|             protected lessonHelper: AddonModLessonHelperProvider, protected lessonSync: AddonModLessonSyncProvider, | ||||
|             protected lessonOfflineProvider: AddonModLessonOfflineProvider, protected cdr: ChangeDetectorRef, | ||||
|             modalCtrl: ModalController, protected navCtrl: NavController, protected appProvider: CoreAppProvider, | ||||
|             protected utils: CoreUtilsProvider, protected urlUtils: CoreUrlUtilsProvider, protected fb: FormBuilder) { | ||||
| 
 | ||||
|         this.lessonId = navParams.get('lessonId'); | ||||
|         this.courseId = navParams.get('courseId'); | ||||
|         this.password = navParams.get('password'); | ||||
|         this.review = !!navParams.get('review'); | ||||
|         this.currentPage = navParams.get('pageId'); | ||||
| 
 | ||||
|         // Block the lesson so it cannot be synced.
 | ||||
|         this.syncProvider.blockOperation(this.component, this.lessonId); | ||||
| 
 | ||||
|         // Create the navigation modal.
 | ||||
|         this.menuModal = modalCtrl.create('AddonModLessonMenuModalPage', { | ||||
|             page: this | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         // Fetch the Lesson data.
 | ||||
|         this.fetchLessonData().then((success) => { | ||||
|             if (success) { | ||||
|                 // Review data loaded or new retake started, remove any retake being finished in sync.
 | ||||
|                 this.lessonSync.deleteRetakeFinishedInSync(this.lessonId); | ||||
|             } | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         // Unblock the lesson so it can be synced.
 | ||||
|         this.syncProvider.unblockOperation(this.component, this.lessonId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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.question && !this.eolData && !this.processData && this.originalData) { | ||||
|             // Question shown. Check if there is any change.
 | ||||
|             if (!this.utils.basicLeftCompare(this.questionForm.getRawValue(), this.originalData, 3)) { | ||||
|                  return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A button was clicked. | ||||
|      * | ||||
|      * @param {any} data Button data. | ||||
|      */ | ||||
|     buttonClicked(data: any): void { | ||||
|         this.processPage(data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Call a function and go offline if allowed and the call fails. | ||||
|      * | ||||
|      * @param {Function} func Function to call. | ||||
|      * @param {any[]} args Arguments to pass to the function. | ||||
|      * @param {number} offlineParamPos Position of the offline parameter in the args. | ||||
|      * @param {number} [jumpsParamPos] Position of the jumps parameter in the args. | ||||
|      * @return {Promise<any>} Promise resolved in success, rejected otherwise. | ||||
|      */ | ||||
|     protected callFunction(func: Function, args: any[], offlineParamPos: number, jumpsParamPos?: number): Promise<any> { | ||||
|         return func.apply(func, args).catch((error) => { | ||||
|             if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson) && | ||||
|                     !this.utils.isWebServiceError(error)) { | ||||
|                 // If it fails, go offline.
 | ||||
|                 this.offline = true; | ||||
| 
 | ||||
|                 // Get the possible jumps now.
 | ||||
|                 return this.lessonProvider.getPagesPossibleJumps(this.lesson.id, true).then((jumpList) => { | ||||
|                     this.jumps = jumpList; | ||||
| 
 | ||||
|                     // Call the function again with offline set to true and the new jumps.
 | ||||
|                     args[offlineParamPos] = true; | ||||
|                     if (typeof jumpsParamPos != 'undefined') { | ||||
|                         args[jumpsParamPos] = this.jumps; | ||||
|                     } | ||||
| 
 | ||||
|                     return func.apply(func, args); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return Promise.reject(error); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Change the page from menu or when continuing from a feedback page. | ||||
|      * | ||||
|      * @param {number} pageId Page to load. | ||||
|      * @param {boolean} [ignoreCurrent] If true, allow loading current page. | ||||
|      */ | ||||
|     changePage(pageId: number, ignoreCurrent?: boolean): void { | ||||
|         if (!ignoreCurrent && !this.eolData && this.currentPage == pageId) { | ||||
|             // Page already loaded, stop.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.loaded = true; | ||||
|         this.messages = []; | ||||
| 
 | ||||
|         this.loadPage(pageId).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error loading page'); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the lesson data and load the page. | ||||
|      * | ||||
|      * @return {Promise<boolean>} Promise resolved with true if success, resolved with false otherwise. | ||||
|      */ | ||||
|     protected fetchLessonData(): Promise<boolean> { | ||||
|         // Wait for any ongoing sync to finish. We won't sync a lesson while it's being played.
 | ||||
|         return this.lessonSync.waitForSync(this.lessonId).then(() => { | ||||
|             return this.lessonProvider.getLessonById(this.courseId, this.lessonId); | ||||
|         }).then((lessonData) => { | ||||
|             this.lesson = lessonData; | ||||
|             this.title = this.lesson.name; // Temporary title.
 | ||||
| 
 | ||||
|             // If lesson has offline data already, use offline mode.
 | ||||
|             return this.lessonOfflineProvider.hasOfflineData(this.lessonId); | ||||
|         }).then((offlineMode) => { | ||||
|             this.offline = offlineMode; | ||||
| 
 | ||||
|             if (!offlineMode && !this.appProvider.isOnline() && this.lessonProvider.isLessonOffline(this.lesson) && !this.review) { | ||||
|                 // Lesson doesn't have offline data, but it allows offline and the device is offline. Use offline mode.
 | ||||
|                 this.offline = true; | ||||
|             } | ||||
| 
 | ||||
|             return this.callFunction(this.lessonProvider.getAccessInformation.bind(this.lessonProvider), | ||||
|                     [this.lesson.id, this.offline, true], 1); | ||||
|         }).then((info) => { | ||||
|             const promises = []; | ||||
| 
 | ||||
|             this.accessInfo = info; | ||||
|             this.canManage = info.canmanage; | ||||
|             this.retake = info.attemptscount; | ||||
|             this.showRetake = !this.currentPage && this.retake > 0; // Only show it in first page if it isn't the first retake.
 | ||||
| 
 | ||||
|             if (info.preventaccessreasons && info.preventaccessreasons.length) { | ||||
|                 // If it's a password protected lesson and we have the password, allow playing it.
 | ||||
|                 if (!this.password || info.preventaccessreasons.length > 1 || !this.lessonProvider.isPasswordProtected(info)) { | ||||
|                     // Lesson cannot be played, show message and go back.
 | ||||
|                     return Promise.reject(info.preventaccessreasons[0].message); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (this.review && this.navParams.get('retake') != info.attemptscount - 1) { | ||||
|                 // Reviewing a retake that isn't the last one. Error.
 | ||||
|                 return Promise.reject(this.translate.instant('addon.mod_lesson.errorreviewretakenotlast')); | ||||
|             } | ||||
| 
 | ||||
|             if (this.password) { | ||||
|                 // Lesson uses password, get the whole lesson object.
 | ||||
|                 promises.push(this.callFunction(this.lessonProvider.getLessonWithPassword.bind(this.lessonProvider), | ||||
|                         [this.lesson.id, this.password, true, this.offline, true], 3).then((lesson) => { | ||||
|                     this.lesson = lesson; | ||||
|                 })); | ||||
|             } | ||||
| 
 | ||||
|             if (this.offline) { | ||||
|                 // Offline mode, get the list of possible jumps to allow navigation.
 | ||||
|                 promises.push(this.lessonProvider.getPagesPossibleJumps(this.lesson.id, true).then((jumpList) => { | ||||
|                     this.jumps = jumpList; | ||||
|                 })); | ||||
|             } | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }).then(() => { | ||||
|             this.mediaFile = this.lesson.mediafiles && this.lesson.mediafiles[0]; | ||||
| 
 | ||||
|             this.lessonWidth = this.lesson.slideshow ? this.domUtils.formatPixelsSize(this.lesson.mediawidth) : ''; | ||||
|             this.lessonHeight = this.lesson.slideshow ? this.domUtils.formatPixelsSize(this.lesson.mediaheight) : ''; | ||||
| 
 | ||||
|             return this.launchRetake(this.currentPage); | ||||
|         }).then(() => { | ||||
|             return true; | ||||
|         }).catch((error) => { | ||||
|             // An error occurred.
 | ||||
|             let promise; | ||||
| 
 | ||||
|             if (this.review && this.navParams.get('retake') && this.utils.isWebServiceError(error)) { | ||||
|                 // The user cannot review the retake. Unmark the retake as being finished in sync.
 | ||||
|                 promise = this.lessonSync.deleteRetakeFinishedInSync(this.lessonId); | ||||
|             } else { | ||||
|                 promise = Promise.resolve(); | ||||
|             } | ||||
| 
 | ||||
|             return promise.then(() => { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); | ||||
|                 this.forceLeave = true; | ||||
|                 this.navCtrl.pop(); | ||||
| 
 | ||||
|                 return false; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Finish the retake. | ||||
|      * | ||||
|      * @param {boolean} [outOfTime] Whether the retake is finished because the user ran out of time. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected finishRetake(outOfTime?: boolean): Promise<any> { | ||||
|         let promise; | ||||
| 
 | ||||
|         this.messages = []; | ||||
| 
 | ||||
|         if (this.offline && this.appProvider.isOnline()) { | ||||
|             // Offline mode but the app is online. Try to sync the data.
 | ||||
|             promise = this.lessonSync.syncLesson(this.lesson.id, true, true).then((result) => { | ||||
|                 if (result.warnings && result.warnings.length) { | ||||
|                     const error = result.warnings[0]; | ||||
| 
 | ||||
|                     // Some data was deleted. Check if the retake has changed.
 | ||||
|                     return this.lessonProvider.getAccessInformation(this.lesson.id).then((info) => { | ||||
|                         if (info.attemptscount != this.accessInfo.attemptscount) { | ||||
|                             // The retake has changed. Leave the view and show the error.
 | ||||
|                             this.forceLeave = true; | ||||
|                             this.navCtrl.pop(); | ||||
| 
 | ||||
|                             return Promise.reject(error); | ||||
|                         } | ||||
| 
 | ||||
|                         // Retake hasn't changed, show the warning and finish the retake in offline.
 | ||||
|                         this.offline = false; | ||||
|                         this.domUtils.showErrorModal(error); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 this.offline = false; | ||||
|             }, () => { | ||||
|                 // Ignore errors.
 | ||||
|             }); | ||||
|         } else { | ||||
|             promise = Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         return promise.then(() => { | ||||
|             // Now finish the retake.
 | ||||
|             const args = [this.lesson, this.courseId, this.password, outOfTime, this.review, this.offline, this.accessInfo]; | ||||
| 
 | ||||
|             return this.callFunction(this.lessonProvider.finishRetake.bind(this.lessonProvider), args, 5); | ||||
|         }).then((data) => { | ||||
|             this.title = this.lesson.name; | ||||
|             this.eolData = data.data; | ||||
|             this.messages = this.messages.concat(data.messages); | ||||
|             this.processData = undefined; | ||||
| 
 | ||||
|             // Format activity link if present.
 | ||||
|             if (this.eolData && this.eolData.activitylink) { | ||||
|                 this.eolData.activitylink.value = this.lessonHelper.formatActivityLink(this.eolData.activitylink.value); | ||||
|             } | ||||
| 
 | ||||
|             // Format review lesson if present.
 | ||||
|             if (this.eolData && this.eolData.reviewlesson) { | ||||
|                 const params = this.urlUtils.extractUrlParams(this.eolData.reviewlesson.value); | ||||
| 
 | ||||
|                 if (!params || !params.pageid) { | ||||
|                     // No pageid in the URL, the user cannot review (probably didn't answer any question).
 | ||||
|                     delete this.eolData.reviewlesson; | ||||
|                 } else { | ||||
|                     this.eolData.reviewlesson.pageid = params.pageid; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Jump to a certain page after performing an action. | ||||
|      * | ||||
|      * @param {number} pageId The page to load. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected jumpToPage(pageId: number): Promise<any> { | ||||
|         if (pageId === 0) { | ||||
|             // Not a valid page, return to entry view.
 | ||||
|             // This happens, for example, when the user clicks to go to previous page and there is no previous page.
 | ||||
|             this.forceLeave = true; | ||||
|             this.navCtrl.pop(); | ||||
| 
 | ||||
|             return Promise.resolve(); | ||||
|         } else if (pageId == AddonModLessonProvider.LESSON_EOL) { | ||||
|             // End of lesson reached.
 | ||||
|             return this.finishRetake(); | ||||
|         } | ||||
| 
 | ||||
|         // Load new page.
 | ||||
|         this.messages = []; | ||||
| 
 | ||||
|         return this.loadPage(pageId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Start or continue a retake. | ||||
|      * | ||||
|      * @param {number} pageId The page to load. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected launchRetake(pageId: number): Promise<any> { | ||||
|         let promise; | ||||
| 
 | ||||
|         if (this.review) { | ||||
|             // Review mode, no need to launch the retake.
 | ||||
|             promise = Promise.resolve({}); | ||||
|         } else if (!this.offline) { | ||||
|             // Not in offline mode, launch the retake.
 | ||||
|             promise = this.lessonProvider.launchRetake(this.lesson.id, this.password, pageId); | ||||
|         } else { | ||||
|             // Check if there is a finished offline retake.
 | ||||
|             promise = this.lessonOfflineProvider.hasFinishedRetake(this.lesson.id).then((finished) => { | ||||
|                 if (finished) { | ||||
|                     // Always show EOL page.
 | ||||
|                     pageId = AddonModLessonProvider.LESSON_EOL; | ||||
|                 } | ||||
| 
 | ||||
|                 return {}; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return promise.then((data) => { | ||||
|             this.currentPage = pageId || this.accessInfo.firstpageid; | ||||
|             this.messages = data.messages || []; | ||||
| 
 | ||||
|             if (this.lesson.timelimit && !this.accessInfo.canmanage) { | ||||
|                 // Get the last lesson timer.
 | ||||
|                 return this.lessonProvider.getTimers(this.lesson.id, false, true).then((timers) => { | ||||
|                     this.endTime = timers[timers.length - 1].starttime + this.lesson.timelimit; | ||||
|                 }); | ||||
|             } | ||||
|         }).then(() => { | ||||
|             return this.loadPage(this.currentPage); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load the lesson menu. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected loadMenu(): Promise<any> { | ||||
|         if (this.loadingMenu) { | ||||
|             // Already loading.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.loadingMenu = true; | ||||
| 
 | ||||
|         const args = [this.lessonId, this.password, this.offline, true]; | ||||
| 
 | ||||
|         return this.callFunction(this.lessonProvider.getPages.bind(this.lessonProvider), args, 2).then((pages) => { | ||||
|             this.lessonPages = pages.map((entry) => { | ||||
|                 return entry.page; | ||||
|             }); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error loading menu.'); | ||||
|         }).finally(() => { | ||||
|             this.loadingMenu = false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load a certain page. | ||||
|      * | ||||
|      * @param {number} pageId The page to load. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected loadPage(pageId: number): Promise<any> { | ||||
|         if (pageId == AddonModLessonProvider.LESSON_EOL) { | ||||
|             // End of lesson reached.
 | ||||
|             return this.finishRetake(); | ||||
|         } | ||||
| 
 | ||||
|         const args = [this.lesson, pageId, this.password, this.review, true, this.offline, true, this.accessInfo, this.jumps]; | ||||
| 
 | ||||
|         return this.callFunction(this.lessonProvider.getPageData.bind(this.lessonProvider), args, 5, 8).then((data) => { | ||||
|             if (data.newpageid == AddonModLessonProvider.LESSON_EOL) { | ||||
|                 // End of lesson reached.
 | ||||
|                 return this.finishRetake(); | ||||
|             } | ||||
| 
 | ||||
|             this.pageData = data; | ||||
|             this.title = data.page.title; | ||||
|             this.pageContent = this.lessonHelper.getPageContentsFromPageData(data); | ||||
|             this.loaded = true; | ||||
|             this.currentPage = pageId; | ||||
|             this.messages = this.messages.concat(data.messages); | ||||
| 
 | ||||
|             // Page loaded, hide EOL and feedback data if shown.
 | ||||
|             this.eolData = this.processData = undefined; | ||||
| 
 | ||||
|             if (this.lessonProvider.isQuestionPage(data.page.type)) { | ||||
|                 // Create an empty FormGroup without controls, they will be added in getQuestionFromPageData.
 | ||||
|                 this.questionForm = this.fb.group({}); | ||||
|                 this.pageButtons = []; | ||||
|                 this.question = this.lessonHelper.getQuestionFromPageData(this.questionForm, data); | ||||
|                 this.originalData = this.questionForm.getRawValue(); // Use getRawValue to include disabled values.
 | ||||
|             } else { | ||||
|                 this.pageButtons = this.lessonHelper.getPageButtonsFromHtml(data.pagecontent); | ||||
|                 this.question = undefined; | ||||
|                 this.originalData = undefined; | ||||
|             } | ||||
| 
 | ||||
|             if (data.displaymenu && !this.displayMenu) { | ||||
|                 // Load the menu.
 | ||||
|                 this.loadMenu(); | ||||
|             } | ||||
|             this.displayMenu = !!data.displaymenu; | ||||
| 
 | ||||
|             if (!this.firstPageLoaded) { | ||||
|                 this.firstPageLoaded = true; | ||||
|             } else { | ||||
|                 this.showRetake = false; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process a page, sending some data. | ||||
|      * | ||||
|      * @param {any} data The data to send. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected processPage(data: any): Promise<any> { | ||||
|         this.loaded = false; | ||||
| 
 | ||||
|         const args = [this.lesson, this.courseId, this.pageData, data, this.password, this.review, this.offline, this.accessInfo, | ||||
|                 this.jumps]; | ||||
| 
 | ||||
|         return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, 6, 8).then((result) => { | ||||
|             if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson)) { | ||||
|                 // Lesson allows offline and the user changed some data in server. Update cached data.
 | ||||
|                 const retake = this.accessInfo.attemptscount; | ||||
| 
 | ||||
|                 if (this.lessonProvider.isQuestionPage(this.pageData.page.type)) { | ||||
|                     this.lessonProvider.getQuestionsAttemptsOnline(this.lessonId, retake, false, undefined, false, true); | ||||
|                 } else { | ||||
|                     this.lessonProvider.getContentPagesViewedOnline(this.lessonId, retake, false, true); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (result.nodefaultresponse || result.inmediatejump) { | ||||
|                 // Don't display feedback or force a redirect to a new page. Load the new page.
 | ||||
|                 return this.jumpToPage(result.newpageid); | ||||
|             } else { | ||||
| 
 | ||||
|                 // Not inmediate jump, show the feedback.
 | ||||
|                 result.feedback = this.lessonHelper.removeQuestionFromFeedback(result.feedback); | ||||
|                 this.messages = result.messages; | ||||
|                 this.processData = result; | ||||
|                 this.processData.buttons = []; | ||||
| 
 | ||||
|                 if (this.lesson.review && !result.correctanswer && !result.noanswer && !result.isessayquestion && | ||||
|                        !result.maxattemptsreached && !result.reviewmode) { | ||||
|                     // User can try again, show button to do so.
 | ||||
|                     this.processData.buttons.push({ | ||||
|                         label: 'addon.mod_lesson.reviewquestionback', | ||||
|                         pageId: this.currentPage | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 // Button to continue.
 | ||||
|                 if (this.lesson.review && !result.correctanswer && !result.noanswer && !result.isessayquestion && | ||||
|                        !result.maxattemptsreached) { | ||||
|                     this.processData.buttons.push({ | ||||
|                         label: 'addon.mod_lesson.reviewquestioncontinue', | ||||
|                         pageId: result.newpageid | ||||
|                     }); | ||||
|                 } else { | ||||
|                     this.processData.buttons.push({ | ||||
|                         label: 'addon.mod_lesson.continue', | ||||
|                         pageId: result.newpageid | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error processing page'); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Review the lesson. | ||||
|      * | ||||
|      * @param {number} pageId Page to load. | ||||
|      */ | ||||
|     reviewLesson(pageId: number): void { | ||||
|         this.loaded = false; | ||||
|         this.review = true; | ||||
|         this.offline = false; // Don't allow offline mode in review.
 | ||||
| 
 | ||||
|         this.loadPage(pageId).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error loading page'); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Submit a question. | ||||
|      */ | ||||
|     submitQuestion(): void { | ||||
|         this.loaded = false; | ||||
| 
 | ||||
|         // Use getRawValue to include disabled values.
 | ||||
|         this.lessonHelper.prepareQuestionData(this.question, this.questionForm.getRawValue()).then((data) => { | ||||
|             return this.processPage(data); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Time up. | ||||
|      */ | ||||
|     timeUp(): void { | ||||
|         // Time up called, hide the timer.
 | ||||
|         this.endTime = undefined; | ||||
|         this.loaded = false; | ||||
| 
 | ||||
|         this.finishRetake(true).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error finishing attempt'); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										159
									
								
								src/addon/mod/lesson/pages/user-retake/user-retake.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/addon/mod/lesson/pages/user-retake/user-retake.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,159 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar> | ||||
|         <ion-title>{{ 'addon.mod_lesson.detailedstats' | translate }}</ion-title> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher [enabled]="loaded" (ionRefresh)="doRefresh($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
| 
 | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <ion-list *ngIf="student"> | ||||
|             <!-- Student data. --> | ||||
|             <a ion-item text-wrap core-user-link [userId]="student.id" [courseId]="courseId" [title]="student.fullname"> | ||||
|                 <ion-avatar *ngIf="student.profileimageurl" item-start> | ||||
|                     <img [src]="student.profileimageurl" [alt]="'core.pictureof' | translate:{$a: student.fullname}" core-external-content role="presentation"> | ||||
|                 </ion-avatar> | ||||
|                 <h2>{{student.fullname}}</h2> | ||||
|                 <core-progress-bar [progress]="student.bestgrade"></core-progress-bar> | ||||
|             </a> | ||||
| 
 | ||||
|             <!-- Retake selector if there is more than one retake. --> | ||||
|             <ion-item text-wrap *ngIf="student.attempts && student.attempts.length > 1"> | ||||
|                 <ion-label id="addon-mod_lesson-retakeslabel">{{ 'addon.mod_lesson.attemptheader' | translate }}</ion-label> | ||||
|                 <ion-select [(ngModel)]="selectedRetake" (ionChange)="changeRetake(selectedRetake)" aria-labelledby="addon-mod_lesson-retakeslabel" interface="popover"> | ||||
|                     <ion-option *ngFor="let retake of student.attempts" [value]="retake.try">{{retake.label}}</ion-option> | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <!-- Retake stats. --> | ||||
|             <div *ngIf="retake && retake.userstats && retake.userstats.gradeinfo" class="addon-mod_lesson-userstats"> | ||||
|                 <ion-item text-wrap> | ||||
|                     <ion-row> | ||||
|                         <ion-col> | ||||
|                             <p class="item-heading">{{ 'addon.mod_lesson.grade' | translate }}</p> | ||||
|                             <p>{{ 'core.percentagenumber' | translate:{$a: retake.userstats.grade} }}</p> | ||||
|                         </ion-col> | ||||
| 
 | ||||
|                         <ion-col> | ||||
|                             <p class="item-heading">{{ 'addon.mod_lesson.rawgrade' | translate }}</p> | ||||
|                             <p>{{ retake.userstats.gradeinfo.earned }} / {{ retake.userstats.gradeinfo.total }}</p> | ||||
|                         </ion-col> | ||||
|                     </ion-row> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap> | ||||
|                     <p class="item-heading">{{ 'addon.mod_lesson.timetaken' | translate }}</p> | ||||
|                     <p>{{ retake.userstats.timetakenReadable }}</p> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap> | ||||
|                     <p class="item-heading">{{ 'addon.mod_lesson.completed' | translate }}</p> | ||||
|                     <p>{{ retake.userstats.completed * 1000 | coreFormatDate:"dfmediumdate" }}</p> | ||||
|                 </ion-item> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- Not completed, no stats. --> | ||||
|             <ion-item text-wrap *ngIf="retake && (!retake.userstats || !retake.userstats.gradeinfo)"> | ||||
|                 {{ 'addon.mod_lesson.notcompleted' | translate }} | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <!-- Pages. --> | ||||
|             <ng-container *ngIf="retake"> | ||||
|                 <!-- The "text-dimmed" class does nothing, but the same goes for the "dimmed" class in Moodle. --> | ||||
|                 <ion-card *ngFor="let page of retake.answerpages" class="addon-mod_lesson-answerpage" [ngClass]="{'text-dimmed': page.grayout}"> | ||||
|                     <ion-card-header text-wrap> | ||||
|                         <h2>{{page.qtype}}: {{page.title}}</h2> | ||||
|                     </ion-card-header> | ||||
|                     <ion-item text-wrap> | ||||
|                         <p class="item-heading">{{ 'addon.mod_lesson.question' | translate }}</p> | ||||
|                         <p><core-format-text [component]="component" [componentId]="lesson.coursemodule" [maxHeight]="50" [text]="page.contents"></core-format-text></p> | ||||
|                     </ion-item> | ||||
|                     <ion-item text-wrap> | ||||
|                         <p class="item-heading">{{ 'addon.mod_lesson.answer' | translate }}</p> | ||||
|                     </ion-item> | ||||
|                     <ion-item text-wrap *ngIf="!page.answerdata || !page.answerdata.answers || !page.answerdata.answers.length"> | ||||
|                         <p>{{ 'addon.mod_lesson.didnotanswerquestion' | translate }}</p> | ||||
|                     </ion-item> | ||||
|                     <div *ngIf="page.answerdata && page.answerdata.answers && page.answerdata.answers.length" class="addon-mod_lesson-answer"> | ||||
|                         <div *ngFor="let answer of page.answerdata.answers"> | ||||
|                             <ion-item text-wrap *ngIf="page.isContent"> | ||||
|                                 <!-- Content page, display a button and the content. --> | ||||
|                                 <ion-row> | ||||
|                                     <ion-col> | ||||
|                                         <button ion-button block color="light" [disabled]="true">{{ answer[0].buttonText }}</button> | ||||
|                                     </ion-col> | ||||
|                                     <ion-col> | ||||
|                                         <p [innerHTML]="answer[0].content"></p> | ||||
|                                     </ion-col> | ||||
|                                 </ion-row> | ||||
|                             </ion-item> | ||||
| 
 | ||||
|                             <div *ngIf="page.isQuestion"> | ||||
|                                 <!-- Question page, show the right input for the answer. --> | ||||
| 
 | ||||
|                                 <!-- Truefalse or matching. --> | ||||
|                                 <ion-item text-wrap *ngIf="answer[0].isCheckbox" [ngClass]="{'addon-mod_lesson-highlight': answer[0].highlight}"> | ||||
|                                     <ion-label> | ||||
|                                         <p><core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="answer[0].content"></core-format-text></p> | ||||
|                                         <ion-badge *ngIf="answer[1]" color="dark"> | ||||
|                                             <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="answer[1]"></core-format-text> | ||||
|                                         </ion-badge> | ||||
|                                     </ion-label> | ||||
|                                     <ion-checkbox [attr.name]="answer[0].name" [ngModel]="answer[0].checked" [disabled]="true" item-end> | ||||
|                                     </ion-checkbox> | ||||
|                                 </ion-item> | ||||
| 
 | ||||
|                                 <!-- Short answer or numeric. --> | ||||
|                                 <ion-item text-wrap *ngIf="answer[0].isText"> | ||||
|                                     <p>{{ answer[0].value }}</p> | ||||
|                                     <ion-badge *ngIf="answer[1]" color="dark"> | ||||
|                                         <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="answer[1]"></core-format-text> | ||||
|                                     </ion-badge> | ||||
|                                 </ion-item> | ||||
| 
 | ||||
|                                 <!-- Matching. --> | ||||
|                                 <ion-item text-wrap *ngIf="answer[0].isSelect"> | ||||
|                                     <ion-row> | ||||
|                                         <ion-col> | ||||
|                                             <p><core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]=" answer[0].content"></core-format-text></p> | ||||
|                                         </ion-col> | ||||
|                                         <ion-col> | ||||
|                                             <p>{{answer[0].value}}</p> | ||||
|                                             <ion-badge *ngIf="answer[1]" color="dark"> | ||||
|                                                 <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="answer[1]"></core-format-text> | ||||
|                                             </ion-badge> | ||||
|                                         </ion-col> | ||||
|                                     </ion-row> | ||||
|                                 </ion-item> | ||||
| 
 | ||||
|                                 <!-- Essay or couldn't determine. --> | ||||
|                                 <ion-item text-wrap *ngIf="!answer[0].isCheckbox && !answer[0].isText && !answer[0].isSelect"> | ||||
|                                     <p><core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="answer[0]"></core-format-text></p> | ||||
|                                     <ion-badge *ngIf="answer[1]" color="dark"> | ||||
|                                         <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="answer[1]"></core-format-text> | ||||
|                                     </ion-badge> | ||||
|                                 </ion-item> | ||||
|                             </div> | ||||
| 
 | ||||
|                             <ion-item text-wrap *ngIf="!page.isContent && !page.isQuestion"> | ||||
|                                 <!-- Another page (end of branch, ...). --> | ||||
|                                 <p><core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="answer[0]"></core-format-text></p> | ||||
|                                 <ion-badge *ngIf="answer[1]" color="dark"> | ||||
|                                     <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="answer[1]"></core-format-text> | ||||
|                                 </ion-badge> | ||||
|                             </ion-item> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <ion-item text-wrap *ngIf="page.answerdata.response"> | ||||
|                             <p class="item-heading">{{ 'addon.mod_lesson.response' | translate }}</p> | ||||
|                             <p><core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="page.answerdata.response"></core-format-text></p> | ||||
|                         </ion-item> | ||||
|                         <ion-item text-wrap *ngIf="page.answerdata.score"> | ||||
|                             <p>{{page.answerdata.score}}</p> | ||||
|                         </ion-item> | ||||
|                     </div> | ||||
|                 </ion-card> | ||||
|             </ng-container> | ||||
|         </ion-list> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										35
									
								
								src/addon/mod/lesson/pages/user-retake/user-retake.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/addon/mod/lesson/pages/user-retake/user-retake.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| // (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 { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CorePipesModule } from '@pipes/pipes.module'; | ||||
| import { AddonModLessonUserRetakePage } from './user-retake'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModLessonUserRetakePage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CorePipesModule, | ||||
|         IonicPageModule.forChild(AddonModLessonUserRetakePage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class AddonModLessonUserRetakePageModule {} | ||||
							
								
								
									
										8
									
								
								src/addon/mod/lesson/pages/user-retake/user-retake.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/addon/mod/lesson/pages/user-retake/user-retake.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| page-addon-mod-lesson-user-retake { | ||||
|     .addon-mod_lesson-highlight { | ||||
|         background: $blue-light; | ||||
|         .label, .label p { | ||||
|             color: $blue-dark; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										228
									
								
								src/addon/mod/lesson/pages/user-retake/user-retake.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								src/addon/mod/lesson/pages/user-retake/user-retake.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,228 @@ | ||||
| // (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, OnInit } from '@angular/core'; | ||||
| import { IonicPage, NavParams } from 'ionic-angular'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { AddonModLessonProvider } from '../../providers/lesson'; | ||||
| import { AddonModLessonHelperProvider } from '../../providers/helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a retake made by a certain user. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-lesson-user-retake' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-lesson-user-retake', | ||||
|     templateUrl: 'user-retake.html', | ||||
| }) | ||||
| export class AddonModLessonUserRetakePage implements OnInit { | ||||
| 
 | ||||
|     component = AddonModLessonProvider.COMPONENT; | ||||
|     lesson: any; // The lesson the retake belongs to.
 | ||||
|     courseId: number; // Course ID the lesson belongs to.
 | ||||
|     selectedRetake: number; // The retake to see.
 | ||||
|     student: any; // Data about the student and his retakes.
 | ||||
|     retake: any; // Data about the retake.
 | ||||
|     loaded: boolean; // Whether the data has been loaded.
 | ||||
| 
 | ||||
|     protected lessonId: number; // The lesson ID the retake belongs to.
 | ||||
|     protected userId: number; // User ID to see the retakes.
 | ||||
|     protected retakeNumber: number; // Number of the initial retake to see.
 | ||||
| 
 | ||||
|     constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected textUtils: CoreTextUtilsProvider, | ||||
|             protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider, | ||||
|             protected userProvider: CoreUserProvider, protected timeUtils: CoreTimeUtilsProvider, | ||||
|             protected lessonProvider: AddonModLessonProvider, protected lessonHelper: AddonModLessonHelperProvider) { | ||||
| 
 | ||||
|         this.lessonId = navParams.get('lessonId'); | ||||
|         this.courseId = navParams.get('courseId'); | ||||
|         this.userId = navParams.get('userId') || sitesProvider.getCurrentSiteUserId(); | ||||
|         this.retakeNumber = navParams.get('retake'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         // Fetch the data.
 | ||||
|         this.fetchData().finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Change the retake displayed. | ||||
|      * | ||||
|      * @param {number} retakeNumber The new retake number. | ||||
|      */ | ||||
|     changeRetake(retakeNumber: number): void { | ||||
|         this.loaded = false; | ||||
| 
 | ||||
|         this.setRetake(retakeNumber).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting attempt.'); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pull to refresh. | ||||
|      * | ||||
|      * @param {any} refresher Refresher. | ||||
|      */ | ||||
|     doRefresh(refresher: any): void { | ||||
|         this.refreshData().finally(() => { | ||||
|             refresher.complete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get lesson and retake data. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchData(): Promise<any> { | ||||
|         return this.lessonProvider.getLessonById(this.courseId, this.lessonId).then((lessonData) => { | ||||
|             this.lesson = lessonData; | ||||
| 
 | ||||
|             // Get the retakes overview for all participants.
 | ||||
|             return this.lessonProvider.getRetakesOverview(this.lesson.id); | ||||
|         }).then((data) => { | ||||
|             // Search the student.
 | ||||
|             let student; | ||||
| 
 | ||||
|             if (data && data.students) { | ||||
|                 for (let i = 0; i < data.students.length; i++) { | ||||
|                     if (data.students[i].id == this.userId) { | ||||
|                         student = data.students[i]; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!student) { | ||||
|                 // Student not found.
 | ||||
|                 return Promise.reject(this.translate.instant('addon.mod_lesson.cannotfinduser')); | ||||
|             } | ||||
| 
 | ||||
|             if (!student.attempts || !student.attempts.length) { | ||||
|                 // No retakes.
 | ||||
|                 return Promise.reject(this.translate.instant('addon.mod_lesson.cannotfindattempt')); | ||||
|             } | ||||
| 
 | ||||
|             student.bestgrade = this.textUtils.roundToDecimals(student.bestgrade, 2); | ||||
|             student.attempts.forEach((retake) => { | ||||
|                 if (this.retakeNumber == retake.try) { | ||||
|                     // The retake specified as parameter exists. Use it.
 | ||||
|                     this.selectedRetake = this.retakeNumber; | ||||
|                 } | ||||
| 
 | ||||
|                 retake.label = this.lessonHelper.getRetakeLabel(retake); | ||||
|             }); | ||||
| 
 | ||||
|             if (!this.selectedRetake) { | ||||
|                 // Retake number not specified or not valid, use the last retake.
 | ||||
|                 this.selectedRetake = student.attempts[student.attempts.length - 1].try; | ||||
|             } | ||||
| 
 | ||||
|             // Get the profile image of the user.
 | ||||
|            return this.userProvider.getProfile(student.id, this.courseId, true).then((user) => { | ||||
|                 student.profileimageurl = user.profileimageurl; | ||||
| 
 | ||||
|                 return student; | ||||
|             }).catch(() => { | ||||
|                 // Error getting profile, resolve promise without adding any extra data.
 | ||||
|                 return student; | ||||
|             }); | ||||
|         }).then((student) => { | ||||
|             this.student = student; | ||||
| 
 | ||||
|             return this.setRetake(this.selectedRetake); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting data.', true); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refreshes data. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected refreshData(): Promise<any> { | ||||
|         const promises = []; | ||||
| 
 | ||||
|         promises.push(this.lessonProvider.invalidateLessonData(this.courseId)); | ||||
|         if (this.lesson) { | ||||
|             promises.push(this.lessonProvider.invalidateRetakesOverview(this.lesson.id)); | ||||
|             promises.push(this.lessonProvider.invalidateUserRetakesForUser(this.lesson.id, this.userId)); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.all(promises).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         }).then(() => { | ||||
|             return this.fetchData(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the retake to view and load its data. | ||||
|      * | ||||
|      * @param {number}retakeNumber Retake number to set. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected setRetake(retakeNumber: number): Promise<any> { | ||||
|         this.selectedRetake = retakeNumber; | ||||
| 
 | ||||
|         return this.lessonProvider.getUserRetake(this.lessonId, retakeNumber, this.userId).then((data) => { | ||||
| 
 | ||||
|             if (data && data.completed != -1) { | ||||
|                 // Completed.
 | ||||
|                 data.userstats.grade = this.textUtils.roundToDecimals(data.userstats.grade, 2); | ||||
|                 data.userstats.timetakenReadable = this.timeUtils.formatTime(data.userstats.timetotake); | ||||
|             } | ||||
| 
 | ||||
|             if (data && data.answerpages) { | ||||
|                 // Format pages data.
 | ||||
|                 data.answerpages.forEach((page) => { | ||||
|                     if (this.lessonProvider.answerPageIsContent(page)) { | ||||
|                         page.isContent = true; | ||||
| 
 | ||||
|                         if (page.answerdata && page.answerdata.answers) { | ||||
|                             page.answerdata.answers.forEach((answer) => { | ||||
|                                 // Content pages only have 1 valid field in the answer array.
 | ||||
|                                 answer[0] = this.lessonHelper.getContentPageAnswerDataFromHtml(answer[0]); | ||||
|                             }); | ||||
|                         } | ||||
|                     } else if (this.lessonProvider.answerPageIsQuestion(page)) { | ||||
|                         page.isQuestion = true; | ||||
| 
 | ||||
|                         if (page.answerdata && page.answerdata.answers) { | ||||
|                             page.answerdata.answers.forEach((answer) => { | ||||
|                                 // Only the first field of the answer array requires to be parsed.
 | ||||
|                                 answer[0] = this.lessonHelper.getQuestionPageAnswerDataFromHtml(answer[0]); | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             this.retake = data; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										95
									
								
								src/addon/mod/lesson/providers/grade-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/addon/mod/lesson/providers/grade-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { NavController } from 'ionic-angular'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreContentLinksModuleGradeHandler } from '@core/contentlinks/classes/module-grade-handler'; | ||||
| import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseHelperProvider } from '@core/course/providers/helper'; | ||||
| import { AddonModLessonProvider } from './lesson'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to lesson grade. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonGradeLinkHandler extends CoreContentLinksModuleGradeHandler { | ||||
|     name = 'AddonModLessonGradeLinkHandler'; | ||||
|     canReview = true; | ||||
| 
 | ||||
|     constructor(courseHelper: CoreCourseHelperProvider, domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider, | ||||
|             protected lessonProvider: AddonModLessonProvider, protected courseProvider: CoreCourseProvider, | ||||
|             protected linkHelper: CoreContentLinksHelperProvider) { | ||||
|         super(courseHelper, domUtils, sitesProvider, 'AddonModLesson', 'lesson'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to the page to review. | ||||
|      * | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} courseId Course ID related to the URL. | ||||
|      * @param {string} siteId Site to use. | ||||
|      * @param {NavController} [navCtrl] Nav Controller to use to navigate. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected goToReview(url: string, params: any, courseId: number, siteId: string, navCtrl?: NavController): Promise<any> { | ||||
| 
 | ||||
|         const moduleId = parseInt(params.id, 10), | ||||
|             modal = this.domUtils.showModalLoading(); | ||||
|         let module; | ||||
| 
 | ||||
|         return this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((mod) => { | ||||
|             module = mod; | ||||
|             courseId = module.course || courseId || params.courseid || params.cid; | ||||
| 
 | ||||
|             // Check if the user can see the user reports in the lesson.
 | ||||
|             return this.lessonProvider.getAccessInformation(module.instance); | ||||
|         }).then((info) => { | ||||
|             if (info.canviewreports) { | ||||
|                 // User can view reports, go to view the report.
 | ||||
|                 const pageParams = { | ||||
|                     courseId: Number(courseId), | ||||
|                     lessonId: module.instance, | ||||
|                     userId: parseInt(params.userid, 10) | ||||
|                 }; | ||||
| 
 | ||||
|                 this.linkHelper.goInSite(navCtrl, 'AddonModLessonUserRetakePage', pageParams, siteId); | ||||
|             } else { | ||||
|                 // User cannot view the report, go to lesson index.
 | ||||
|                 this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section); | ||||
|             } | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); | ||||
|         }).finally(() => { | ||||
|             modal.dismiss(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled for a certain site (site + user) and a URL. | ||||
|      * If not defined, defaults to true. | ||||
|      * | ||||
|      * @param {string} siteId The site ID. | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} [courseId] Course ID related to the URL. Optional but recommended. | ||||
|      * @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site. | ||||
|      */ | ||||
|     isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> { | ||||
|         return this.lessonProvider.isPluginEnabled(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										494
									
								
								src/addon/mod/lesson/providers/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										494
									
								
								src/addon/mod/lesson/providers/helper.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,494 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { FormBuilder, FormGroup } from '@angular/forms'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { AddonModLessonProvider } from './lesson'; | ||||
| import * as moment from 'moment'; | ||||
| 
 | ||||
| /** | ||||
|  * Helper service that provides some features for quiz. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonHelperProvider { | ||||
| 
 | ||||
|     protected div = document.createElement('div'); // A div element to search in HTML code.
 | ||||
| 
 | ||||
|     constructor(private domUtils: CoreDomUtilsProvider, private fb: FormBuilder, private translate: TranslateService, | ||||
|             private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Given the HTML of next activity link, format it to extract the href and the text. | ||||
|      * | ||||
|      * @param {string} activityLink HTML of the activity link. | ||||
|      * @return {{formatted: boolean, label: string, href: string}} Formatted data. | ||||
|      */ | ||||
|     formatActivityLink(activityLink: string): {formatted: boolean, label: string, href: string} { | ||||
|         this.div.innerHTML = activityLink; | ||||
|         const anchor = this.div.querySelector('a'); | ||||
|         if (!anchor) { | ||||
|             // Anchor not found, return the original HTML.
 | ||||
|             return { | ||||
|                 formatted: false, | ||||
|                 label: activityLink, | ||||
|                 href: '' | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             formatted: true, | ||||
|             label: anchor.innerHTML, | ||||
|             href: anchor.href | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given the HTML of an answer from a content page, extract the data to render the answer. | ||||
|      * | ||||
|      * @param  {String} html Answer's HTML. | ||||
|      * @return {{buttonText: string, content: string}} Data to render the answer. | ||||
|      */ | ||||
|     getContentPageAnswerDataFromHtml(html: string): {buttonText: string, content: string} { | ||||
|         const data = { | ||||
|                 buttonText: '', | ||||
|                 content: '' | ||||
|             }; | ||||
| 
 | ||||
|         // Search the input button.
 | ||||
|         this.div.innerHTML = html; | ||||
|         const button = <HTMLInputElement> this.div.querySelector('input[type="button"]'); | ||||
| 
 | ||||
|         if (button) { | ||||
|             // Extract the button content and remove it from the HTML.
 | ||||
|             data.buttonText = button.value; | ||||
|             button.remove(); | ||||
|         } | ||||
| 
 | ||||
|         data.content = this.div.innerHTML.trim(); | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the buttons to change pages. | ||||
|      * | ||||
|      * @param {string} html Page's HTML. | ||||
|      * @return {any[]} List of buttons. | ||||
|      */ | ||||
|     getPageButtonsFromHtml(html: string): any[] { | ||||
|         const buttons = []; | ||||
| 
 | ||||
|         // Get the container of the buttons if it exists.
 | ||||
|         this.div.innerHTML = html; | ||||
|         let buttonsContainer = this.div.querySelector('.branchbuttoncontainer'); | ||||
| 
 | ||||
|         if (!buttonsContainer) { | ||||
|             // Button container not found, might be a legacy lesson (from 1.9).
 | ||||
|             if (!this.div.querySelector('form input[type="submit"]')) { | ||||
|                 // No buttons found.
 | ||||
|                 return buttons; | ||||
|             } | ||||
|             buttonsContainer = this.div; | ||||
|         } | ||||
| 
 | ||||
|         const forms = Array.from(buttonsContainer.querySelectorAll('form')); | ||||
|         forms.forEach((form) => { | ||||
|             const buttonSelector = 'input[type="submit"], button[type="submit"]', | ||||
|                 buttonEl = <HTMLInputElement | HTMLButtonElement> form.querySelector(buttonSelector), | ||||
|                 inputs = Array.from(form.querySelectorAll('input')); | ||||
| 
 | ||||
|             if (!buttonEl || !inputs || !inputs.length) { | ||||
|                 // Button not found or no inputs, ignore it.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const button = { | ||||
|                 id: buttonEl.id, | ||||
|                 title: buttonEl.title || buttonEl.value, | ||||
|                 content: buttonEl.tagName == 'INPUT' ? buttonEl.value : buttonEl.innerHTML.trim(), | ||||
|                 data: {} | ||||
|             }; | ||||
| 
 | ||||
|             inputs.forEach((input) => { | ||||
|                 if (input.type != 'submit') { | ||||
|                     button.data[input.name] = input.value; | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             buttons.push(button); | ||||
|         }); | ||||
| 
 | ||||
|         return buttons; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a page data (result of getPageData), get the page contents. | ||||
|      * | ||||
|      * @param {any} data Page data. | ||||
|      * @return {string} Page contents. | ||||
|      */ | ||||
|     getPageContentsFromPageData(data: any): string { | ||||
|         // Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered.
 | ||||
|         this.div.innerHTML = data.pagecontent; | ||||
|         const contents = this.div.querySelector('.contents'); | ||||
| 
 | ||||
|         if (contents) { | ||||
|             return contents.innerHTML.trim(); | ||||
|         } | ||||
| 
 | ||||
|         // Cannot find contents element, return the page.contents (some elements like videos might not work).
 | ||||
|         return data.page.contents; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a question and all the data required to render it from the page data (result of AddonModLessonProvider.getPageData). | ||||
|      * | ||||
|      * @param {FormGroup} questionForm The form group where to add the controls. | ||||
|      * @param {any} pageData Page data (result of $mmaModLesson#getPageData). | ||||
|      * @return {any} Question data. | ||||
|      */ | ||||
|     getQuestionFromPageData(questionForm: FormGroup, pageData: any): any { | ||||
|         const question: any = {}; | ||||
| 
 | ||||
|         // Get the container of the question answers if it exists.
 | ||||
|         this.div.innerHTML = pageData.pagecontent; | ||||
|         const fieldContainer = this.div.querySelector('.fcontainer'); | ||||
| 
 | ||||
|         // Get hidden inputs and add their data to the form group.
 | ||||
|         const hiddenInputs = <HTMLInputElement[]> Array.from(this.div.querySelectorAll('input[type="hidden"]')); | ||||
|         hiddenInputs.forEach((input) => { | ||||
|             questionForm.addControl(input.name, this.fb.control(input.value)); | ||||
|         }); | ||||
| 
 | ||||
|         // Get the submit button and extract its value.
 | ||||
|         const submitButton = <HTMLInputElement> this.div.querySelector('input[type="submit"]'); | ||||
|         question.submitLabel = submitButton ? submitButton.value : this.translate.instant('addon.mod_lesson.submit'); | ||||
| 
 | ||||
|         if (!fieldContainer) { | ||||
|             // Element not found, return.
 | ||||
|             return question; | ||||
|         } | ||||
| 
 | ||||
|         let type; | ||||
| 
 | ||||
|         switch (pageData.page.qtype) { | ||||
|             case AddonModLessonProvider.LESSON_PAGE_TRUEFALSE: | ||||
|             case AddonModLessonProvider.LESSON_PAGE_MULTICHOICE: | ||||
|                 question.template = 'multichoice'; | ||||
|                 question.options = []; | ||||
| 
 | ||||
|                 // Get all the inputs. Search radio first.
 | ||||
|                 let inputs = <HTMLInputElement[]> Array.from(fieldContainer.querySelectorAll('input[type="radio"]')); | ||||
|                 if (!inputs || !inputs.length) { | ||||
|                     // Radio buttons not found, it might be a multi answer. Search for checkbox.
 | ||||
|                     question.multi = true; | ||||
|                     inputs = <HTMLInputElement[]> Array.from(fieldContainer.querySelectorAll('input[type="checkbox"]')); | ||||
| 
 | ||||
|                     if (!inputs || !inputs.length) { | ||||
|                         // No checkbox found either. Stop.
 | ||||
|                         return question; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 let controlAdded = false; | ||||
|                 inputs.forEach((input) => { | ||||
|                     const option: any = { | ||||
|                             id: input.id, | ||||
|                             name: input.name, | ||||
|                             value: input.value, | ||||
|                             checked: !!input.checked, | ||||
|                             disabled: !!input.disabled | ||||
|                         }, | ||||
|                         parent = input.parentElement; | ||||
| 
 | ||||
|                     if (option.checked || question.multi) { | ||||
|                         // Add the control.
 | ||||
|                         const value = question.multi ? {value: option.checked, disabled: option.disabled} : option.value; | ||||
|                         questionForm.addControl(option.name, this.fb.control(value)); | ||||
|                         controlAdded = true; | ||||
|                     } | ||||
| 
 | ||||
|                     // Remove the input and use the rest of the parent contents as the label.
 | ||||
|                     input.remove(); | ||||
|                     option.text = parent.innerHTML.trim(); | ||||
| 
 | ||||
|                     question.options.push(option); | ||||
|                 }); | ||||
| 
 | ||||
|                 if (!question.multi) { | ||||
|                     question.controlName = inputs[0].name; // All option have the same name in single choice.
 | ||||
| 
 | ||||
|                     if (!controlAdded) { | ||||
|                         // No checked option for single choice, add the control with an empty value.
 | ||||
|                         questionForm.addControl(question.controlName, this.fb.control('')); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 break; | ||||
| 
 | ||||
|             case AddonModLessonProvider.LESSON_PAGE_NUMERICAL: | ||||
|                 type = 'number'; | ||||
|             case AddonModLessonProvider.LESSON_PAGE_SHORTANSWER: | ||||
|                 question.template = 'shortanswer'; | ||||
| 
 | ||||
|                 // Get the input.
 | ||||
|                 const input = <HTMLInputElement> fieldContainer.querySelector('input[type="text"], input[type="number"]'); | ||||
|                 if (!input) { | ||||
|                     return question; | ||||
|                 } | ||||
| 
 | ||||
|                 question.input = { | ||||
|                     id: input.id, | ||||
|                     name: input.name, | ||||
|                     maxlength: input.maxLength, | ||||
|                     type: type || 'text' | ||||
|                 }; | ||||
| 
 | ||||
|                 // Init the control.
 | ||||
|                 questionForm.addControl(input.name, this.fb.control({value: input.value, disabled: input.readOnly})); | ||||
|                 break; | ||||
| 
 | ||||
|             case AddonModLessonProvider.LESSON_PAGE_ESSAY: | ||||
|                 question.template = 'essay'; | ||||
| 
 | ||||
|                 // Get the textarea.
 | ||||
|                 const textarea = fieldContainer.querySelector('textarea'); | ||||
| 
 | ||||
|                 if (!textarea) { | ||||
|                     // Textarea not found, probably review mode.
 | ||||
|                     const answerEl = fieldContainer.querySelector('.reviewessay'); | ||||
|                     if (!answerEl) { | ||||
|                         // Answer not found, stop.
 | ||||
|                         return question; | ||||
|                     } | ||||
|                     question.useranswer = answerEl.innerHTML; | ||||
| 
 | ||||
|                 } else { | ||||
|                     question.textarea = { | ||||
|                         id: textarea.id, | ||||
|                         name: textarea.name || 'answer[text]' | ||||
|                     }; | ||||
| 
 | ||||
|                     // Init the control.
 | ||||
|                     question.control = this.fb.control(''); | ||||
|                     questionForm.addControl(question.textarea.name, question.control); | ||||
|                 } | ||||
| 
 | ||||
|                 break; | ||||
| 
 | ||||
|             case AddonModLessonProvider.LESSON_PAGE_MATCHING: | ||||
|                 question.template = 'matching'; | ||||
| 
 | ||||
|                 const rows = Array.from(fieldContainer.querySelectorAll('.answeroption')); | ||||
|                 question.rows = []; | ||||
| 
 | ||||
|                 rows.forEach((row) => { | ||||
|                     const label = row.querySelector('label'), | ||||
|                         select = row.querySelector('select'), | ||||
|                         options = Array.from(row.querySelectorAll('option')), | ||||
|                         rowData: any = {}; | ||||
| 
 | ||||
|                     if (!label || !select || !options || !options.length) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // Get the row's text (label).
 | ||||
|                     rowData.text = label.innerHTML.trim(); | ||||
|                     rowData.id = select.id; | ||||
|                     rowData.name = select.name; | ||||
|                     rowData.options = []; | ||||
| 
 | ||||
|                     // Treat each option.
 | ||||
|                     let controlAdded = false; | ||||
|                     options.forEach((option) => { | ||||
|                         if (typeof option.value == 'undefined') { | ||||
|                             // Option not valid, ignore it.
 | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         const opt = { | ||||
|                             value: option.value, | ||||
|                             label: option.innerHTML.trim(), | ||||
|                             selected: option.selected | ||||
|                         }; | ||||
| 
 | ||||
|                         if (opt.selected) { | ||||
|                             controlAdded = true; | ||||
|                             questionForm.addControl(rowData.name, this.fb.control({value: opt.value, disabled: !!select.disabled})); | ||||
|                         } | ||||
| 
 | ||||
|                         rowData.options.push(opt); | ||||
|                     }); | ||||
| 
 | ||||
|                     if (!controlAdded) { | ||||
|                         // No selected option, add the control with an empty value.
 | ||||
|                         questionForm.addControl(rowData.name, this.fb.control({value: '', disabled: !!select.disabled})); | ||||
|                     } | ||||
| 
 | ||||
|                     question.rows.push(rowData); | ||||
|                 }); | ||||
|                 break; | ||||
|             default: | ||||
|                 // Nothing to do.
 | ||||
|         } | ||||
| 
 | ||||
|         return question; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given the HTML of an answer from a question page, extract the data to render the answer. | ||||
|      * | ||||
|      * @param {string} html Answer's HTML. | ||||
|      * @return {any} Object with the data to render the answer. If the answer doesn't require any parsing, return a string with | ||||
|      *               the HTML. | ||||
|      */ | ||||
|     getQuestionPageAnswerDataFromHtml(html: string): any { | ||||
|         const data: any = {}; | ||||
| 
 | ||||
|         this.div.innerHTML = html; | ||||
| 
 | ||||
|         // Check if it has a checkbox.
 | ||||
|         let input = <HTMLInputElement> this.div.querySelector('input[type="checkbox"][name*="answer"]'); | ||||
| 
 | ||||
|         if (input) { | ||||
|             // Truefalse or multichoice.
 | ||||
|             data.isCheckbox = true; | ||||
|             data.checked = !!input.checked; | ||||
|             data.name = input.name; | ||||
|             data.highlight = !!this.div.querySelector('.highlight'); | ||||
| 
 | ||||
|             input.remove(); | ||||
|             data.content = this.div.innerHTML.trim(); | ||||
| 
 | ||||
|             return data; | ||||
|         } | ||||
| 
 | ||||
|         // Check if it has an input text or number.
 | ||||
|         input = <HTMLInputElement> this.div.querySelector('input[type="number"],input[type="text"]'); | ||||
|         if (input) { | ||||
|             // Short answer or numeric.
 | ||||
|             data.isText = true; | ||||
|             data.value = input.value; | ||||
| 
 | ||||
|             return data; | ||||
|         } | ||||
| 
 | ||||
|         // Check if it has a select.
 | ||||
|         const select = <HTMLSelectElement> this.div.querySelector('select'); | ||||
|         if (select && select.options) { | ||||
|             // Matching.
 | ||||
|             const selectedOption = select.options[select.selectedIndex]; | ||||
|             data.isSelect = true; | ||||
|             data.id = select.id; | ||||
|             if (selectedOption) { | ||||
|                 data.value = selectedOption.value; | ||||
|             } else { | ||||
|                 data.value = ''; | ||||
|             } | ||||
| 
 | ||||
|             select.remove(); | ||||
|             data.content = this.div.innerHTML.trim(); | ||||
| 
 | ||||
|             return data; | ||||
|         } | ||||
| 
 | ||||
|         // The answer doesn't need any parsing, return the HTML as it is.
 | ||||
|         return html; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a label to identify a retake (lesson attempt). | ||||
|      * | ||||
|      * @param {any} retake Retake object. | ||||
|      * @param {boolean} [includeDuration] Whether to include the duration of the retake. | ||||
|      * @return {string} Retake label. | ||||
|      */ | ||||
|     getRetakeLabel(retake: any, includeDuration?: boolean): string { | ||||
|         const data = { | ||||
|                 retake: retake.try + 1, | ||||
|                 grade: '', | ||||
|                 timestart: '', | ||||
|                 duration: '' | ||||
|             }, | ||||
|             hasGrade = retake.grade != null; | ||||
| 
 | ||||
|         if (hasGrade || retake.end) { | ||||
|             // Retake finished with or without grade (if the lesson only has content pages, it has no grade).
 | ||||
|             if (hasGrade) { | ||||
|                 data.grade = this.translate.instant('core.percentagenumber', {$a: retake.grade}); | ||||
|             } | ||||
|             data.timestart = moment(retake.timestart * 1000).format('LLL'); | ||||
|             if (includeDuration) { | ||||
|                 data.duration = this.timeUtils.formatTime(retake.timeend - retake.timestart); | ||||
|             } | ||||
|         } else { | ||||
|             // The user has not completed the retake.
 | ||||
|             data.grade = this.translate.instant('addon.mod_lesson.notcompleted'); | ||||
|             if (retake.timestart) { | ||||
|                 data.timestart = moment(retake.timestart * 1000).format('LLL'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return this.translate.instant('addon.mod_lesson.retakelabel' + (includeDuration ? 'full' : 'short'), data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare the question data to be sent to server. | ||||
|      * | ||||
|      * @param {any} question Question to prepare. | ||||
|      * @param {any} data Data to prepare. | ||||
|      * @return {Promise<any>} Promise resolved with the data to send when done. | ||||
|      */ | ||||
|     prepareQuestionData(question: any, data: any): Promise<any> { | ||||
|         if (question.template == 'essay' && question.textarea) { | ||||
|             // The answer might need formatting. Check if rich text editor is enabled or not.
 | ||||
|             return this.domUtils.isRichTextEditorEnabled().then((enabled) => { | ||||
|                 if (!enabled) { | ||||
|                     // Rich text editor not enabled, add some HTML to the answer if needed.
 | ||||
|                     data[question.textarea.property] = this.textUtils.formatHtmlLines(data[question.textarea.property]); | ||||
|                 } | ||||
| 
 | ||||
|                 return data; | ||||
|             }); | ||||
|         } else if (question.template == 'multichoice' && question.multi) { | ||||
|             // Only send the options with value set to true.
 | ||||
|             for (const name in data) { | ||||
|                 if (name.match(/answer\[\d+\]/) && data[name] == false) { | ||||
|                     delete data[name]; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return Promise.resolve(data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given the feedback of a process page in HTML, remove the question text. | ||||
|      * | ||||
|      * @param {string} html Feedback's HTML. | ||||
|      * @return {string} Feedback without the question text. | ||||
|      */ | ||||
|     removeQuestionFromFeedback(html: string): string { | ||||
|         this.div.innerHTML = html; | ||||
| 
 | ||||
|         // Remove the question text.
 | ||||
|         this.domUtils.removeElement(this.div, '.generalbox:not(.feedback):not(.correctanswer)'); | ||||
| 
 | ||||
|         return this.div.innerHTML.trim(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										105
									
								
								src/addon/mod/lesson/providers/index-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/addon/mod/lesson/providers/index-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; | ||||
| import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseHelperProvider } from '@core/course/providers/helper'; | ||||
| import { AddonModLessonProvider } from './lesson'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to lesson index. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonIndexLinkHandler extends CoreContentLinksModuleIndexHandler { | ||||
|     name = 'AddonModLessonIndexLinkHandler'; | ||||
| 
 | ||||
|     constructor(courseHelper: CoreCourseHelperProvider, protected lessonProvider: AddonModLessonProvider, | ||||
|             protected domUtils: CoreDomUtilsProvider, protected courseProvider: CoreCourseProvider) { | ||||
|         super(courseHelper, 'AddonModLesson', 'lesson'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the list of actions for a link (url). | ||||
|      * | ||||
|      * @param {string[]} siteIds List of sites the URL belongs to. | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} [courseId] Course ID related to the URL. Optional but recommended. | ||||
|      * @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions. | ||||
|      */ | ||||
|     getActions(siteIds: string[], url: string, params: any, courseId?: number): | ||||
|             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { | ||||
| 
 | ||||
|         courseId = courseId || params.courseid || params.cid; | ||||
| 
 | ||||
|         return [{ | ||||
|             action: (siteId, navCtrl?): void => { | ||||
|                 /* Ignore the pageid param. If we open the lesson player with a certain page and the user hasn't started | ||||
|                    the lesson, an error is thrown: could not find lesson_timer records. */ | ||||
|                 if (params.userpassword) { | ||||
|                     this.navigateToModuleWithPassword(parseInt(params.id, 10), courseId, params.userpassword, siteId); | ||||
|                 } else { | ||||
|                     this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); | ||||
|                 } | ||||
|             } | ||||
|         }]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled for a certain site (site + user) and a URL. | ||||
|      * If not defined, defaults to true. | ||||
|      * | ||||
|      * @param {string} siteId The site ID. | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} [courseId] Course ID related to the URL. Optional but recommended. | ||||
|      * @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site. | ||||
|      */ | ||||
|     isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> { | ||||
|         return this.lessonProvider.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Navigate to a lesson module (index page) with a fixed password. | ||||
|      * | ||||
|      * @param {number} moduleId Module ID. | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @param {string} password Password. | ||||
|      * @param {string} siteId Site ID. | ||||
|      * @return {Promise<any>} Promise resolved when navigated. | ||||
|      */ | ||||
|     protected navigateToModuleWithPassword(moduleId: number, courseId: number, password: string, siteId: string): Promise<any> { | ||||
|         const modal = this.domUtils.showModalLoading(); | ||||
| 
 | ||||
|         // Get the module.
 | ||||
|         return this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => { | ||||
|             courseId = courseId || module.course; | ||||
| 
 | ||||
|             // Store the password so it's automatically used.
 | ||||
|             return this.lessonProvider.storePassword(parseInt(module.instance, 10), password, siteId).catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             }).then(() => { | ||||
|                 return this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section); | ||||
|             }); | ||||
|         }).catch(() => { | ||||
|             // Error, go to index page.
 | ||||
|             return this.courseHelper.navigateToModule(moduleId, siteId, courseId); | ||||
|         }).finally(() => { | ||||
|             modal.dismiss(); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										598
									
								
								src/addon/mod/lesson/providers/lesson-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										598
									
								
								src/addon/mod/lesson/providers/lesson-offline.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,598 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { AddonModLessonProvider } from './lesson'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle offline lesson. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonOfflineProvider { | ||||
| 
 | ||||
|     protected logger; | ||||
| 
 | ||||
|     // Variables for database. We use lowercase in the names to match the WS responses.
 | ||||
|     protected RETAKES_TABLE = 'addon_mod_lesson_retakes'; | ||||
|     protected PAGE_ATTEMPTS_TABLE = 'addon_mod_lesson_page_attempts'; | ||||
|     protected tablesSchema = [ | ||||
|         { | ||||
|             name: this.RETAKES_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'lessonid', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true // Only 1 offline retake per lesson.
 | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'retake', // Retake number.
 | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'finished', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'outoftime', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'lastquestionpage', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             name: this.PAGE_ATTEMPTS_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'lessonid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'retake', // Retake number.
 | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'pageid', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER', | ||||
|                     notNull: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'data', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'type', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'newpageid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'correct', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'answerid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'useranswer', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|             ], | ||||
|             primaryKeys: ['lessonid', 'retake', 'pageid', 'timemodified'] // A user can attempt several times per page and retake.
 | ||||
|         } | ||||
|     ]; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider, | ||||
|             private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider) { | ||||
|         this.logger = logger.getInstance('AddonModLessonOfflineProvider'); | ||||
| 
 | ||||
|         this.sitesProvider.createTablesFromSchema(this.tablesSchema); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete an offline attempt. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake Lesson retake number. | ||||
|      * @param {number} pageId Page ID. | ||||
|      * @param {number} timemodified The timemodified of the attempt. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     deleteAttempt(lessonId: number, retake: number, pageId: number, timemodified: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().deleteRecords(this.PAGE_ATTEMPTS_TABLE, { | ||||
|                 lessonid: lessonId, | ||||
|                 retake: retake, | ||||
|                 pageid: pageId, | ||||
|                 timemodified: timemodified | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete offline lesson retake. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     deleteRetake(lessonId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().deleteRecords(this.RETAKES_TABLE, {lessonid: lessonId}); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete offline attempts for a retake and page. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake Lesson retake number. | ||||
|      * @param {number} pageId Page ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     deleteRetakeAttemptsForPage(lessonId: number, retake: number, pageId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().deleteRecords(this.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId, retake: retake, pageid: pageId}); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Mark a retake as finished. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} courseId Course ID the lesson belongs to. | ||||
|      * @param {number} retake Retake number. | ||||
|      * @param {boolean} finished  Whether retake is finished. | ||||
|      * @param {boolean} outOfTime If the user ran out of time. | ||||
|      * @param {string} [siteId]   Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}           Promise resolved in success, rejected otherwise. | ||||
|      */ | ||||
|     finishRetake(lessonId: number, courseId: number, retake: number, finished?: boolean, outOfTime?: boolean, siteId?: string) | ||||
|             : Promise<any> { | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             // Get current stored retake (if any). If not found, it will create a new one.
 | ||||
|             return this.getRetakeWithFallback(lessonId, courseId, retake, site.id).then((entry) => { | ||||
|                 entry.finished = finished ? 1 : 0; | ||||
|                 entry.outoftime = outOfTime ? 1 : 0; | ||||
|                 entry.timemodified = this.timeUtils.timestamp(); | ||||
| 
 | ||||
|                 return site.getDb().insertRecord(this.RETAKES_TABLE, entry); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all the offline page attempts in a certain site. | ||||
|      * | ||||
|      * @param {string} [siteId] Site ID. If not set, use current site. | ||||
|      * @return {Promise<any>} Promise resolved when the offline attempts are retrieved. | ||||
|      */ | ||||
|     getAllAttempts(siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSiteDb(siteId).then((db) => { | ||||
|             return db.getAllRecords(this.PAGE_ATTEMPTS_TABLE); | ||||
|         }).then((attempts) => { | ||||
|             return this.parsePageAttempts(attempts); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all the lessons that have offline data in a certain site. | ||||
|      * | ||||
|      * @param {string} [siteId] Site ID. If not set, use current site. | ||||
|      * @return {Promise<any>} Promise resolved with an object containing the lessons. | ||||
|      */ | ||||
|     getAllLessonsWithData(siteId?: string): Promise<any> { | ||||
|         const promises = [], | ||||
|             lessons = {}; | ||||
| 
 | ||||
|         // Get the lessons from page attempts.
 | ||||
|         promises.push(this.getAllAttempts(siteId).then((entries) => { | ||||
|             this.getLessonsFromEntries(lessons, entries); | ||||
|         }).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         })); | ||||
| 
 | ||||
|         // Get the lessons from retakes.
 | ||||
|         promises.push(this.getAllRetakes(siteId).then((entries) => { | ||||
|             this.getLessonsFromEntries(lessons, entries); | ||||
|         }).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         })); | ||||
| 
 | ||||
|         return Promise.all(promises).then(() => { | ||||
|             return this.utils.objectToArray(lessons); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all the offline retakes in a certain site. | ||||
|      * | ||||
|      * @param {string} [siteId] Site ID. If not set, use current site. | ||||
|      * @return {Promise<any>} Promise resolved when the offline retakes are retrieved. | ||||
|      */ | ||||
|     getAllRetakes(siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSiteDb(siteId).then((db) => { | ||||
|             return db.getAllRecords(this.RETAKES_TABLE); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve the last offline attempt stored in a retake. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake Retake number. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved with the attempt (undefined if no attempts). | ||||
|      */ | ||||
|     getLastQuestionPageAttempt(lessonId: number, retake: number, siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         return this.getRetakeWithFallback(lessonId, 0, retake, siteId).then((retakeData) => { | ||||
|             if (!retakeData.lastquestionpage) { | ||||
|                 // No question page attempted.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             return this.getRetakeAttemptsForPage(lessonId, retake, retakeData.lastquestionpage, siteId).then((attempts) => { | ||||
|                 // Return the attempt with highest timemodified.
 | ||||
|                 return attempts.reduce((a, b) => { | ||||
|                     return a.timemodified > b.timemodified ? a : b; | ||||
|                 }); | ||||
|             }); | ||||
|         }).catch(() => { | ||||
|             // Error, return undefined.
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve all offline attempts for a lesson. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any[]>} Promise resolved with the attempts. | ||||
|      */ | ||||
|     getLessonAttempts(lessonId: number, siteId?: string): Promise<any[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecords(this.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId}); | ||||
|         }).then((attempts) => { | ||||
|             return this.parsePageAttempts(attempts); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a list of DB entries (either retakes or page attempts), get the list of lessons. | ||||
|      * | ||||
|      * @param {any} lessons Object where to store the lessons. | ||||
|      * @param {any[]} entries List of DB entries. | ||||
|      */ | ||||
|     protected getLessonsFromEntries(lessons: any, entries: any[]): void { | ||||
|         entries.forEach((entry) => { | ||||
|             if (!lessons[entry.lessonid]) { | ||||
|                 lessons[entry.lessonid] = { | ||||
|                     id: entry.lessonid, | ||||
|                     courseId: entry.courseid | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get attempts for question pages and retake in a lesson. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake Retake number. | ||||
|      * @param {boolean} [correct] True to only fetch correct attempts, false to get them all. | ||||
|      * @param {number} [pageId] If defined, only get attempts on this page. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any[]>}  Promise resolved with the attempts. | ||||
|      */ | ||||
|     getQuestionsAttempts(lessonId: number, retake: number, correct?: boolean, pageId?: number, siteId?: string): Promise<any[]> { | ||||
|         let promise; | ||||
| 
 | ||||
|         if (pageId) { | ||||
|             // Page ID is set, only get the attempts for that page.
 | ||||
|             promise = this.getRetakeAttemptsForPage(lessonId, retake, pageId, siteId); | ||||
|         } else { | ||||
|             // Page ID not specified, get all the attempts.
 | ||||
|             promise = this.getRetakeAttemptsForType(lessonId, retake, AddonModLessonProvider.TYPE_QUESTION, siteId); | ||||
|         } | ||||
| 
 | ||||
|         return promise.then((attempts) => { | ||||
|             if (correct) { | ||||
|                 return attempts.filter((attempt) => { | ||||
|                     return !!attempt.correct; | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return attempts; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve a retake from site DB. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved with the retake. | ||||
|      */ | ||||
|     getRetake(lessonId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecord(this.RETAKES_TABLE, {lessonid: lessonId}); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve all offline attempts for a retake. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake   Retake number. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any[]>} Promise resolved with the retake attempts. | ||||
|      */ | ||||
|     getRetakeAttempts(lessonId: number, retake: number, siteId?: string): Promise<any[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecords(this.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId, retake: retake}); | ||||
|         }).then((attempts) => { | ||||
|             return this.parsePageAttempts(attempts); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve offline attempts for a retake and page. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake   Lesson retake number. | ||||
|      * @param {number} pageId   Page ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}         Promise resolved with the retake attempts. | ||||
|      */ | ||||
|     getRetakeAttemptsForPage(lessonId: number, retake: number, pageId: number, siteId?: string): Promise<any[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecords(this.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId, retake: retake, pageid: pageId}); | ||||
|         }).then((attempts) => { | ||||
|             return this.parsePageAttempts(attempts); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve offline attempts for certain pages for a retake. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake   Retake number. | ||||
|      * @param {number} type     Type of the pages to get: TYPE_QUESTION or TYPE_STRUCTURE. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}         Promise resolved with the retake attempts. | ||||
|      */ | ||||
|     getRetakeAttemptsForType(lessonId: number, retake: number, type: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecords(this.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId, retake: retake, type: type}); | ||||
|         }).then((attempts) => { | ||||
|             return this.parsePageAttempts(attempts); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get stored retake. If not found or doesn't match the retake number, return a new one. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} courseId Course ID the lesson belongs to. | ||||
|      * @param {number} retake Retake number. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved with the retake. | ||||
|      */ | ||||
|     protected getRetakeWithFallback(lessonId: number, courseId: number, retake: number, siteId?: string): Promise<any> { | ||||
|         // Get current stored retake.
 | ||||
|         return this.getRetake(lessonId, siteId).then((retakeData) => { | ||||
|             if (retakeData.retake != retake) { | ||||
|                 // The stored retake doesn't match the retake number, create a new one.
 | ||||
|                 return Promise.reject(null); | ||||
|             } | ||||
| 
 | ||||
|             return retakeData; | ||||
|         }).catch(() => { | ||||
|             // No retake, create a new one.
 | ||||
|             return { | ||||
|                 lessonid: lessonId, | ||||
|                 retake: retake, | ||||
|                 courseid: courseId, | ||||
|                 finished: 0 | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there is a finished retake for a certain lesson. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>} Promise resolved with boolean. | ||||
|      */ | ||||
|     hasFinishedRetake(lessonId: number, siteId?: string): Promise<boolean> { | ||||
|         return this.getRetake(lessonId, siteId).then((retake) => { | ||||
|             return !!retake.finished; | ||||
|         }).catch(() => { | ||||
|             return false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a lesson has offline data. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>} Promise resolved with boolean. | ||||
|      */ | ||||
|     hasOfflineData(lessonId: number, siteId?: string): Promise<boolean> { | ||||
|         const promises = []; | ||||
|         let hasData = false; | ||||
| 
 | ||||
|         promises.push(this.getRetake(lessonId, siteId).then(() => { | ||||
|             hasData = true; | ||||
|         }).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         })); | ||||
| 
 | ||||
|         promises.push(this.getLessonAttempts(lessonId, siteId).then((attempts) => { | ||||
|             hasData = hasData || !!attempts.length; | ||||
|         }).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         })); | ||||
| 
 | ||||
|         return Promise.all(promises).then(() => { | ||||
|             return hasData; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there are offline attempts for a retake. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake Retake number. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>} Promise resolved with a boolean. | ||||
|      */ | ||||
|     hasRetakeAttempts(lessonId: number, retake: number, siteId?: string): Promise<boolean> { | ||||
|         return this.getRetakeAttempts(lessonId, retake, siteId).then((list) => { | ||||
|             return !!list.length; | ||||
|         }).catch(() => { | ||||
|             return false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse some properties of a page attempt. | ||||
|      * | ||||
|      * @param {any} attempt The attempt to treat. | ||||
|      * @return {any} The treated attempt. | ||||
|      */ | ||||
|     protected parsePageAttempt(attempt: any): any { | ||||
|         attempt.data = this.textUtils.parseJSON(attempt.data); | ||||
|         attempt.useranswer = this.textUtils.parseJSON(attempt.useranswer); | ||||
| 
 | ||||
|         return attempt; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse some properties of some page attempts. | ||||
|      * | ||||
|      * @param {any[]} attempts The attempts to treat. | ||||
|      * @return {any[]} The treated attempts. | ||||
|      */ | ||||
|     protected parsePageAttempts(attempts: any[]): any[] { | ||||
|         attempts.forEach((attempt) => { | ||||
|             this.parsePageAttempt(attempt); | ||||
|         }); | ||||
| 
 | ||||
|         return attempts; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process a lesson page, saving its data. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} courseId Course ID the lesson belongs to. | ||||
|      * @param {number} retake Retake number. | ||||
|      * @param {any} page Page. | ||||
|      * @param {any} data Data to save. | ||||
|      * @param {number} newPageId New page ID (calculated). | ||||
|      * @param {number} [answerId] The answer ID that the user answered. | ||||
|      * @param {boolean} [correct] If answer is correct. Only for question pages. | ||||
|      * @param {any} [userAnswer] The user's answer (userresponse from checkAnswer). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved in success, rejected otherwise. | ||||
|      */ | ||||
|     processPage(lessonId: number, courseId: number, retake: number, page: any, data: any, newPageId: number, answerId?: number, | ||||
|             correct?: boolean, userAnswer?: any, siteId?: string): Promise<any> { | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const entry = { | ||||
|                 lessonid: lessonId, | ||||
|                 retake: retake, | ||||
|                 pageid: page.id, | ||||
|                 timemodified: this.timeUtils.timestamp(), | ||||
|                 courseid: courseId, | ||||
|                 data: data ? JSON.stringify(data) : null, | ||||
|                 type: page.type, | ||||
|                 newpageid: newPageId, | ||||
|                 correct: correct ? 1 : 0, | ||||
|                 answerid: Number(answerId), | ||||
|                 useranswer: userAnswer ? JSON.stringify(userAnswer) : null, | ||||
|             }; | ||||
| 
 | ||||
|             return site.getDb().insertRecord(this.PAGE_ATTEMPTS_TABLE, entry); | ||||
|         }).then(() => { | ||||
|             if (page.type == AddonModLessonProvider.TYPE_QUESTION) { | ||||
|                 // It's a question page, set it as last question page attempted.
 | ||||
|                 return this.setLastQuestionPageAttempted(lessonId, courseId, retake, page.id, siteId); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the last question page attempted in a retake. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} courseId Course ID the lesson belongs to. | ||||
|      * @param {number} retake Retake number. | ||||
|      * @param {number} lastPage ID of the last question page attempted. | ||||
|      * @param {string} [siteId]  Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved in success, rejected otherwise. | ||||
|      */ | ||||
|     setLastQuestionPageAttempted(lessonId: number, courseId: number, retake: number, lastPage: number, siteId?: string) | ||||
|             : Promise<any> { | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             // Get current stored retake (if any). If not found, it will create a new one.
 | ||||
|             return this.getRetakeWithFallback(lessonId, courseId, retake, site.id).then((entry) => { | ||||
|                 entry.lastquestionpage = lastPage; | ||||
|                 entry.timemodified = this.timeUtils.timestamp(); | ||||
| 
 | ||||
|                 return site.getDb().insertRecord(this.RETAKES_TABLE, entry); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										498
									
								
								src/addon/mod/lesson/providers/lesson-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										498
									
								
								src/addon/mod/lesson/providers/lesson-sync.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,498 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSyncProvider } from '@providers/sync'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreUrlUtilsProvider } from '@providers/utils/url'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||
| import { AddonModLessonProvider } from './lesson'; | ||||
| import { AddonModLessonOfflineProvider } from './lesson-offline'; | ||||
| import { AddonModLessonPrefetchHandler } from './prefetch-handler'; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by a lesson sync. | ||||
|  */ | ||||
| export interface AddonModLessonSyncResult { | ||||
|     /** | ||||
|      * List of warnings. | ||||
|      * @type {string[]} | ||||
|      */ | ||||
|     warnings: string[]; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether some data was sent to the server or offline data was updated. | ||||
|      * @type {boolean} | ||||
|      */ | ||||
|     updated: boolean; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Service to sync lesson. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { | ||||
| 
 | ||||
|     static AUTO_SYNCED = 'addon_mod_lesson_autom_synced'; | ||||
|     static SYNC_TIME = 300000; | ||||
| 
 | ||||
|     protected componentTranslate: string; | ||||
| 
 | ||||
|     // Variables for database.
 | ||||
|     protected RETAKES_FINISHED_TABLE = 'addon_mod_lesson_retakes_finished_sync'; | ||||
|     protected tablesSchema = { | ||||
|         name: this.RETAKES_FINISHED_TABLE, | ||||
|         columns: [ | ||||
|             { | ||||
|                 name: 'lessonId', | ||||
|                 type: 'INTEGER', | ||||
|                 primaryKey: true | ||||
|             }, | ||||
|             { | ||||
|                 name: 'retake', | ||||
|                 type: 'INTEGER' | ||||
|             }, | ||||
|             { | ||||
|                 name: 'pageId', | ||||
|                 type: 'INTEGER' | ||||
|             }, | ||||
|             { | ||||
|                 name: 'timefinished', | ||||
|                 type: 'INTEGER' | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
| 
 | ||||
|     constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, | ||||
|             syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, | ||||
|             courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, | ||||
|             private lessonProvider: AddonModLessonProvider, private lessonOfflineProvider: AddonModLessonOfflineProvider, | ||||
|             private prefetchHandler: AddonModLessonPrefetchHandler, private timeUtils: CoreTimeUtilsProvider, | ||||
|             private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider) { | ||||
| 
 | ||||
|         super('AddonModLessonSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); | ||||
| 
 | ||||
|         this.componentTranslate = courseProvider.translateModuleName('lesson'); | ||||
| 
 | ||||
|         this.sitesProvider.createTableFromSchema(this.tablesSchema); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Unmark a retake as finished in a synchronization. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     deleteRetakeFinishedInSync(lessonId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().deleteRecords(this.RETAKES_FINISHED_TABLE, {lessonId}); | ||||
|         }).catch(() => { | ||||
|             // Ignore errors, maybe there is none.
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a retake finished in a synchronization for a certain lesson (if any). | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved with the retake entry (undefined if no retake). | ||||
|      */ | ||||
|     getRetakeFinishedInSync(lessonId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecord(this.RETAKES_FINISHED_TABLE, {lessonId}); | ||||
|         }).catch(() => { | ||||
|             // Ignore errors, return undefined.
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a lesson has data to synchronize. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake  Retake number. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>} Promise resolved with boolean: whether it has data to sync. | ||||
|      */ | ||||
|     hasDataToSync(lessonId: number, retake: number, siteId?: string): Promise<boolean> { | ||||
|         const promises = []; | ||||
|         let hasDataToSync = false; | ||||
| 
 | ||||
|         promises.push(this.lessonOfflineProvider.hasRetakeAttempts(lessonId, retake, siteId).then((hasAttempts) => { | ||||
|             hasDataToSync = hasDataToSync || hasAttempts; | ||||
|         }).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         })); | ||||
| 
 | ||||
|         promises.push(this.lessonOfflineProvider.hasFinishedRetake(lessonId, siteId).then((hasFinished) => { | ||||
|             hasDataToSync = hasDataToSync || hasFinished; | ||||
|         })); | ||||
| 
 | ||||
|         return Promise.all(promises).then(() => { | ||||
|             return hasDataToSync; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Mark a retake as finished in a synchronization. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {number} retake The retake number. | ||||
|      * @param {number} pageId The page ID to start reviewing from. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     setRetakeFinishedInSync(lessonId: number, retake: number, pageId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().insertRecord(this.RETAKES_FINISHED_TABLE, { | ||||
|                 lessonId: lessonId, | ||||
|                 retake: Number(retake), | ||||
|                 pageId: Number(pageId), | ||||
|                 timefinished: this.timeUtils.timestamp() | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize all the lessons in a certain site or in all sites. | ||||
|      * | ||||
|      * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. | ||||
|      * @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     syncAllLessons(siteId?: string): Promise<any> { | ||||
|         return this.syncOnSites('all lessons', this.syncAllLessonsFunc.bind(this), [], siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync all lessons on a site. | ||||
|      * | ||||
|      * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. | ||||
|      * @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     protected syncAllLessonsFunc(siteId?: string): Promise<any> { | ||||
|         // Get all the lessons that have something to be synchronized.
 | ||||
|         return this.lessonOfflineProvider.getAllLessonsWithData(siteId).then((lessons) => { | ||||
|             // Sync all lessons that haven't been synced for a while.
 | ||||
|             const promises = []; | ||||
| 
 | ||||
|             lessons.forEach((lesson) => { | ||||
|                 promises.push(this.syncLessonIfNeeded(lesson.id, false, siteId).then((result) => { | ||||
|                     if (result && result.updated) { | ||||
|                         // Sync successful, send event.
 | ||||
|                         this.eventsProvider.trigger(AddonModLessonSyncProvider.AUTO_SYNCED, { | ||||
|                             lessonId: lesson.id, | ||||
|                             warnings: result.warnings | ||||
|                         }, siteId); | ||||
|                     } | ||||
|                 })); | ||||
|             }); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync a lesson only if a certain time has passed since the last time. | ||||
|      * | ||||
|      * @param {any} lessonId Lesson ID. | ||||
|      * @param {boolean} [askPreflight] Whether we should ask for password if needed. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when the lesson is synced or if it doesn't need to be synced. | ||||
|      */ | ||||
|     syncLessonIfNeeded(lessonId: number, askPassword?: boolean, siteId?: string): Promise<any> { | ||||
|         return this.isSyncNeeded(lessonId, siteId).then((needed) => { | ||||
|             if (needed) { | ||||
|                 return this.syncLesson(lessonId, askPassword, false, siteId); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize a lesson. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {boolean} askPassword True if we should ask for password if needed, false otherwise. | ||||
|      * @param {boolean} ignoreBlock True to ignore the sync block setting. | ||||
|      * @param {string} [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<AddonModLessonSyncResult>} Promise resolved in success. | ||||
|      */ | ||||
|     syncLesson(lessonId: number, askPassword?: boolean, ignoreBlock?: boolean, siteId?: string): Promise<AddonModLessonSyncResult> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         const result: AddonModLessonSyncResult = { | ||||
|                 warnings: [], | ||||
|                 updated: false | ||||
|             }; | ||||
|         let syncPromise, | ||||
|             lesson, | ||||
|             courseId, | ||||
|             password, | ||||
|             accessInfo; | ||||
| 
 | ||||
|         if (this.isSyncing(lessonId, siteId)) { | ||||
|             // There's already a sync ongoing for this lesson, return the promise.
 | ||||
|             return this.getOngoingSync(lessonId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         // Verify that lesson isn't blocked.
 | ||||
|         if (!ignoreBlock && this.syncProvider.isBlocked(AddonModLessonProvider.COMPONENT, lessonId, siteId)) { | ||||
|             this.logger.debug('Cannot sync lesson ' + lessonId + ' because it is blocked.'); | ||||
| 
 | ||||
|             return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Try to sync lesson ' + lessonId + ' in site ' + siteId); | ||||
| 
 | ||||
|         // Try to synchronize the attempts first.
 | ||||
|         syncPromise = this.lessonOfflineProvider.getLessonAttempts(lessonId, siteId).then((attempts) => { | ||||
|             if (!attempts.length) { | ||||
|                 return; | ||||
|             } else if (!this.appProvider.isOnline()) { | ||||
|                 // Cannot sync in offline.
 | ||||
|                 return Promise.reject(null); | ||||
|             } | ||||
| 
 | ||||
|             courseId = attempts[0].courseid; | ||||
| 
 | ||||
|             // Get the info, access info and the lesson password if needed.
 | ||||
|             return this.lessonProvider.getLessonById(courseId, lessonId, false, siteId).then((lessonData) => { | ||||
|                 lesson = lessonData; | ||||
| 
 | ||||
|                 return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); | ||||
|             }).then((data) => { | ||||
|                 const attemptsLength = attempts.length, | ||||
|                     promises = []; | ||||
| 
 | ||||
|                 accessInfo = data.accessInfo; | ||||
|                 password = data.password; | ||||
|                 lesson = data.lesson || lesson; | ||||
| 
 | ||||
|                 // Filter the attempts, get only the ones that belong to the current retake.
 | ||||
|                 attempts = attempts.filter((attempt) => { | ||||
|                     if (attempt.retake != accessInfo.attemptscount) { | ||||
|                         // Attempt doesn't belong to current retake, delete.
 | ||||
|                         promises.push(this.lessonOfflineProvider.deleteAttempt(lesson.id, attempt.retake, attempt.pageid, | ||||
|                                 attempt.timemodified, siteId).catch(() => { | ||||
|                             // Ignore errors.
 | ||||
|                         })); | ||||
| 
 | ||||
|                         return false; | ||||
|                     } | ||||
| 
 | ||||
|                     return true; | ||||
|                 }); | ||||
| 
 | ||||
|                 if (attempts.length != attemptsLength) { | ||||
|                     // Some attempts won't be sent, add a warning.
 | ||||
|                     result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { | ||||
|                         component: this.componentTranslate, | ||||
|                         name: lesson.name, | ||||
|                         error: this.translate.instant('addon.mod_lesson.warningretakefinished') | ||||
|                     })); | ||||
|                 } | ||||
| 
 | ||||
|                 return Promise.all(promises); | ||||
|             }).then(() => { | ||||
|                 if (!attempts.length) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 // Send the attempts in the same order they were answered.
 | ||||
|                 attempts.sort((a, b) => { | ||||
|                     return a.timemodified - b.timemodified; | ||||
|                 }); | ||||
| 
 | ||||
|                 attempts = attempts.map((attempt) => { | ||||
|                     return { | ||||
|                         func: this.sendAttempt.bind(this), | ||||
|                         params: [lesson, password, attempt, result, siteId], | ||||
|                         blocking: true | ||||
|                     }; | ||||
|                 }); | ||||
| 
 | ||||
|                 return this.utils.executeOrderedPromises(attempts); | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             // Attempts sent or there was none. If there is a finished retake, send it.
 | ||||
|             return this.lessonOfflineProvider.getRetake(lessonId, siteId).then((retake) => { | ||||
|                 if (!retake.finished) { | ||||
|                     // The retake isn't marked as finished, nothing to send. Delete the retake.
 | ||||
|                     return this.lessonOfflineProvider.deleteRetake(lessonId, siteId); | ||||
|                 } else if (!this.appProvider.isOnline()) { | ||||
|                     // Cannot sync in offline.
 | ||||
|                     return Promise.reject(null); | ||||
|                 } | ||||
| 
 | ||||
|                 let promise; | ||||
| 
 | ||||
|                 courseId = retake.courseid || courseId; | ||||
| 
 | ||||
|                 if (lesson) { | ||||
|                     // Data already retrieved when syncing attempts.
 | ||||
|                     promise = Promise.resolve(); | ||||
|                 } else { | ||||
|                     promise = this.lessonProvider.getLessonById(courseId, lessonId, false, siteId).then((lessonData) => { | ||||
|                         lesson = lessonData; | ||||
| 
 | ||||
|                         return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); | ||||
|                     }).then((data) => { | ||||
|                         accessInfo = data.accessInfo; | ||||
|                         password = data.password; | ||||
|                         lesson = data.lesson || lesson; | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 return promise.then(() => { | ||||
|                     if (retake.retake != accessInfo.attemptscount) { | ||||
|                         // The retake changed, add a warning if it isn't there already.
 | ||||
|                         if (!result.warnings.length) { | ||||
|                             result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { | ||||
|                                 component: this.componentTranslate, | ||||
|                                 name: lesson.name, | ||||
|                                 error: this.translate.instant('addon.mod_lesson.warningretakefinished') | ||||
|                             })); | ||||
|                         } | ||||
| 
 | ||||
|                         return this.lessonOfflineProvider.deleteRetake(lessonId, siteId); | ||||
|                     } | ||||
| 
 | ||||
|                     // All good, finish the retake.
 | ||||
|                     return this.lessonProvider.finishRetakeOnline(lessonId, password, false, false, siteId).then((response) => { | ||||
|                         result.updated = true; | ||||
| 
 | ||||
|                         if (!ignoreBlock) { | ||||
|                             // Mark the retake as finished in a sync if it can be reviewed.
 | ||||
|                             if (response.data && response.data.reviewlesson) { | ||||
|                                 const params = this.urlUtils.extractUrlParams(response.data.reviewlesson.value); | ||||
|                                 if (params && params.pageid) { | ||||
|                                     // The retake can be reviewed, mark it as finished. Don't block the user for this.
 | ||||
|                                     this.setRetakeFinishedInSync(lessonId, retake.retake, params.pageid, siteId); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         return this.lessonOfflineProvider.deleteRetake(lessonId, siteId); | ||||
|                     }).catch((error) => { | ||||
|                         if (error && this.utils.isWebServiceError(error)) { | ||||
|                             // The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
 | ||||
|                             result.updated = true; | ||||
| 
 | ||||
|                             return this.lessonOfflineProvider.deleteRetake(lessonId, siteId).then(() => { | ||||
|                                 // Retake deleted, add a warning.
 | ||||
|                                 result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { | ||||
|                                     component: this.componentTranslate, | ||||
|                                     name: lesson.name, | ||||
|                                     error: error | ||||
|                                 })); | ||||
|                             }); | ||||
|                         } else { | ||||
|                             // Couldn't connect to server, reject.
 | ||||
|                             return Promise.reject(error); | ||||
|                         } | ||||
|                     }); | ||||
|                 }); | ||||
|             }, () => { | ||||
|                 // No retake stored, nothing to do.
 | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             if (result.updated && courseId) { | ||||
|                 // Data has been sent to server. Now invalidate the WS calls.
 | ||||
|                 const promises = []; | ||||
| 
 | ||||
|                 promises.push(this.lessonProvider.invalidateAccessInformation(lessonId, siteId)); | ||||
|                 promises.push(this.lessonProvider.invalidateContentPagesViewed(lessonId, siteId)); | ||||
|                 promises.push(this.lessonProvider.invalidateQuestionsAttempts(lessonId, siteId)); | ||||
|                 promises.push(this.lessonProvider.invalidatePagesPossibleJumps(lessonId, siteId)); | ||||
|                 promises.push(this.lessonProvider.invalidateTimers(lessonId, siteId)); | ||||
| 
 | ||||
|                 return this.utils.allPromises(promises).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }).then(() => { | ||||
|                     // Sync successful, update some data that might have been modified.
 | ||||
|                     return this.lessonProvider.getAccessInformation(lessonId, false, false, siteId).then((info) => { | ||||
|                         const promises = [], | ||||
|                             retake = info.attemptscount; | ||||
| 
 | ||||
|                         promises.push(this.lessonProvider.getContentPagesViewedOnline(lessonId, retake, false, false, siteId)); | ||||
|                         promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lessonId, retake, false, undefined, false, | ||||
|                                 false, siteId)); | ||||
| 
 | ||||
|                         return Promise.all(promises); | ||||
|                     }).catch(() => { | ||||
|                         // Ignore errors.
 | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|         }).then(() => { | ||||
|             // Sync finished, set sync time.
 | ||||
|             return this.setSyncTime(lessonId, siteId).catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             // All done, return the result.
 | ||||
|             return result; | ||||
|         }); | ||||
| 
 | ||||
|         return this.addOngoingSync(lessonId, syncPromise, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Send an attempt to the site and delete it afterwards. | ||||
|      * | ||||
|      * @param {any} lesson Lesson. | ||||
|      * @param {string} password Password (if any). | ||||
|      * @param {any} attempt Attempt to send. | ||||
|      * @param {AddonModLessonSyncResult} result Result where to store the data. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected sendAttempt(lesson: any, password: string, attempt: any, result: AddonModLessonSyncResult, siteId?: string) | ||||
|             : Promise<any> { | ||||
| 
 | ||||
|         return this.lessonProvider.processPageOnline(lesson.id, attempt.pageid, attempt.data, password, false, siteId).then(() => { | ||||
|             result.updated = true; | ||||
| 
 | ||||
|             return this.lessonOfflineProvider.deleteAttempt(lesson.id, attempt.retake, attempt.pageid, attempt.timemodified, | ||||
|                     siteId); | ||||
|         }).catch((error) => { | ||||
|             if (error && this.utils.isWebServiceError(error)) { | ||||
|                 // The WebService has thrown an error, this means that the attempt cannot be submitted. Delete it.
 | ||||
|                 result.updated = true; | ||||
| 
 | ||||
|                 return this.lessonOfflineProvider.deleteAttempt(lesson.id, attempt.retake, attempt.pageid, attempt.timemodified, | ||||
|                         siteId).then(() => { | ||||
| 
 | ||||
|                     // Attempt deleted, add a warning.
 | ||||
|                     result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { | ||||
|                         component: this.componentTranslate, | ||||
|                         name: lesson.name, | ||||
|                         error: error | ||||
|                     })); | ||||
|                 }); | ||||
|             } else { | ||||
|                 // Couldn't connect to server, reject.
 | ||||
|                 return Promise.reject(error); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3235
									
								
								src/addon/mod/lesson/providers/lesson.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3235
									
								
								src/addon/mod/lesson/providers/lesson.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										72
									
								
								src/addon/mod/lesson/providers/module-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/addon/mod/lesson/providers/module-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { NavController, NavOptions } from 'ionic-angular'; | ||||
| import { AddonModLessonIndexComponent } from '../components/index/index'; | ||||
| import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { AddonModLessonProvider } from './lesson'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to support quiz modules. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonModuleHandler implements CoreCourseModuleHandler { | ||||
|     name = 'AddonModLesson'; | ||||
|     modName = 'lesson'; | ||||
| 
 | ||||
|     constructor(private courseProvider: CoreCourseProvider, private lessonProvider: AddonModLessonProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return {Promise<boolean>} Promise resolved with boolean: whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     isEnabled(): Promise<boolean> { | ||||
|         return this.lessonProvider.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the data required to display the module in the course contents view. | ||||
|      * | ||||
|      * @param {any} module The module object. | ||||
|      * @param {number} courseId The course ID. | ||||
|      * @param {number} sectionId The section ID. | ||||
|      * @return {CoreCourseModuleHandlerData} Data to render the module. | ||||
|      */ | ||||
|     getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { | ||||
|         return { | ||||
|             icon: this.courseProvider.getModuleIconSrc('lesson'), | ||||
|             title: module.name, | ||||
|             class: 'addon-mod_lesson-handler', | ||||
|             showDownloadButton: true, | ||||
|             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { | ||||
|                 navCtrl.push('AddonModLessonIndexPage', {module: module, courseId: courseId}, options); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the component to render the module. This is needed to support singleactivity course format. | ||||
|      * The component returned must implement CoreCourseModuleMainComponent. | ||||
|      * | ||||
|      * @param {any} course The course object. | ||||
|      * @param {any} module The module object. | ||||
|      * @return {any} The component to use, undefined if not found. | ||||
|      */ | ||||
|     getMainComponent(course: any, module: any): any { | ||||
|         return AddonModLessonIndexComponent; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										449
									
								
								src/addon/mod/lesson/providers/prefetch-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										449
									
								
								src/addon/mod/lesson/providers/prefetch-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,449 @@ | ||||
| // (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 { Injectable, Injector } from '@angular/core'; | ||||
| import { ModalController } from 'ionic-angular'; | ||||
| import { CoreGroupsProvider } from '@providers/groups'; | ||||
| import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; | ||||
| import { AddonModLessonProvider } from './lesson'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to prefetch lessons. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { | ||||
|     name = 'AddonModLesson'; | ||||
|     modName = 'lesson'; | ||||
|     component = AddonModLessonProvider.COMPONENT; | ||||
|     // Don't check timers to decrease positives. If a user performs some action it will be reflected in other items.
 | ||||
|     updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^pages$|^answers$|^questionattempts$|^pagesviewed$/; | ||||
| 
 | ||||
|     constructor(protected injector: Injector, protected modalCtrl: ModalController, protected groupsProvider: CoreGroupsProvider, | ||||
|             protected lessonProvider: AddonModLessonProvider) { | ||||
|         super(injector); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Ask password. | ||||
|      * | ||||
|      * @param {any} info Lesson access info. | ||||
|      * @return {Promise<string>} Promise resolved with the password. | ||||
|      */ | ||||
|     protected askUserPassword(info: any): Promise<string> { | ||||
|         // Create and show the modal.
 | ||||
|         const modal = this.modalCtrl.create('AddonModLessonPasswordModalPage'); | ||||
| 
 | ||||
|         modal.present(); | ||||
| 
 | ||||
|         // Wait for modal to be dismissed.
 | ||||
|         return new Promise((resolve, reject): void => { | ||||
|             modal.onDidDismiss((password) => { | ||||
|                 if (typeof password != 'undefined') { | ||||
|                     resolve(password); | ||||
|                 } else { | ||||
|                     reject(this.domUtils.createCanceledError()); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download the module. | ||||
|      * | ||||
|      * @param {any} module The module object returned by WS. | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. | ||||
|      * @return {Promise<any>} Promise resolved when all content is downloaded. | ||||
|      */ | ||||
|     download(module: any, courseId: number, dirPath?: string): Promise<any> { | ||||
|         // Same implementation for download and prefetch.
 | ||||
|         return this.prefetch(module, courseId, false, dirPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the download size of a module. | ||||
|      * | ||||
|      * @param {any} module Module. | ||||
|      * @param {Number} courseId Course ID the module belongs to. | ||||
|      * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. | ||||
|      * @return {Promise<{size: number, total: boolean}>} Promise resolved with the size and a boolean indicating if it was able | ||||
|      *                                                   to calculate the total size. | ||||
|      */ | ||||
|     getDownloadSize(module: any, courseId: any, single?: boolean): Promise<{ size: number, total: boolean }> { | ||||
|         const siteId = this.sitesProvider.getCurrentSiteId(); | ||||
|         let lesson, | ||||
|             password, | ||||
|             result; | ||||
| 
 | ||||
|         return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lessonData) => { | ||||
|             lesson = lessonData; | ||||
| 
 | ||||
|             // Get the lesson password if it's needed.
 | ||||
|             return this.getLessonPassword(lesson.id, false, true, single, siteId); | ||||
|         }).then((data) => { | ||||
|             password = data.password; | ||||
|             lesson = data.lesson || lesson; | ||||
| 
 | ||||
|             // Get intro files and media files.
 | ||||
|             let files = lesson.mediafiles || []; | ||||
|             files = files.concat(this.getIntroFilesFromInstance(module, lesson)); | ||||
| 
 | ||||
|             result = this.utils.sumFileSizes(files); | ||||
| 
 | ||||
|             // Get the pages to calculate the size.
 | ||||
|             return this.lessonProvider.getPages(lesson.id, password, false, false, siteId); | ||||
|         }).then((pages) => { | ||||
|             pages.forEach((page) => { | ||||
|                 result.size += page.filessizetotal; | ||||
|             }); | ||||
| 
 | ||||
|             return result; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get list of files. If not defined, we'll assume they're in module.contents. | ||||
|      * | ||||
|      * @param {any} module Module. | ||||
|      * @param {Number} courseId Course ID the module belongs to. | ||||
|      * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. | ||||
|      * @return {Promise<any[]>} Promise resolved with the list of files. | ||||
|      */ | ||||
|     getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> { | ||||
|         return Promise.resolve([]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the lesson password if needed. If not stored, it can ask the user to enter it. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {boolean} [forceCache] Whether it should return cached data. Has priority over ignoreCache. | ||||
|      * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {boolean} [askPassword] True if we should ask for password if needed, false otherwise. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<{password?: string, lesson?: any, accessInfo: any}>} Promise resolved when done. | ||||
|      */ | ||||
|     getLessonPassword(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, askPassword?: boolean, siteId?: string) | ||||
|             : Promise<{password?: string, lesson?: any, accessInfo: any}> { | ||||
| 
 | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Get access information to check if password is needed.
 | ||||
|         return this.lessonProvider.getAccessInformation(lessonId, forceCache, ignoreCache, siteId).then((info): any => { | ||||
|             if (info.preventaccessreasons && info.preventaccessreasons.length) { | ||||
|                 const passwordNeeded = info.preventaccessreasons.length == 1 && this.lessonProvider.isPasswordProtected(info); | ||||
|                 if (passwordNeeded) { | ||||
| 
 | ||||
|                     // The lesson requires a password. Check if there is one in DB.
 | ||||
|                     return this.lessonProvider.getStoredPassword(lessonId).catch(() => { | ||||
|                         // No password found.
 | ||||
|                     }).then((password) => { | ||||
|                         if (password) { | ||||
|                             return this.validatePassword(lessonId, info, password, forceCache, ignoreCache, siteId); | ||||
|                         } else { | ||||
|                             return Promise.reject(null); | ||||
|                         } | ||||
|                     }).catch(() => { | ||||
|                         // No password or error validating it. Ask for it if allowed.
 | ||||
|                         if (askPassword) { | ||||
|                             return this.askUserPassword(info).then((password) => { | ||||
|                                 return this.validatePassword(lessonId, info, password, forceCache, ignoreCache, siteId); | ||||
|                             }); | ||||
|                         } | ||||
| 
 | ||||
|                         // Cannot ask for password, reject.
 | ||||
|                         return Promise.reject(info.preventaccessreasons[0].message); | ||||
|                     }); | ||||
|                 } else  { | ||||
|                     // Lesson cannot be played, reject.
 | ||||
|                     return Promise.reject(info.preventaccessreasons[0].message); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Password not needed.
 | ||||
|             return { accessInfo: info }; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate the prefetched content. | ||||
|      * | ||||
|      * @param {number} moduleId The module ID. | ||||
|      * @param {number} courseId The course ID the module belongs to. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateContent(moduleId: number, courseId: number): Promise<any> { | ||||
|         // Only invalidate the data that doesn't ignore cache when prefetching.
 | ||||
|         const promises = []; | ||||
| 
 | ||||
|         promises.push(this.lessonProvider.invalidateLessonData(courseId)); | ||||
|         promises.push(this.courseProvider.invalidateModule(moduleId)); | ||||
|         promises.push(this.groupsProvider.invalidateActivityAllowedGroups(moduleId)); | ||||
| 
 | ||||
|         return Promise.all(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate WS calls needed to determine module status. | ||||
|      * | ||||
|      * @param {any} module Module. | ||||
|      * @param {number} courseId Course ID the module belongs to. | ||||
|      * @return {Promise<any>} Promise resolved when invalidated. | ||||
|      */ | ||||
|     invalidateModule(module: any, courseId: number): Promise<any> { | ||||
|         const siteId = this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Invalidate data to determine if module is downloadable.
 | ||||
|         return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lesson) => { | ||||
|             const promises = []; | ||||
| 
 | ||||
|             promises.push(this.lessonProvider.invalidateLessonData(courseId, siteId)); | ||||
|             promises.push(this.lessonProvider.invalidateAccessInformation(lesson.id, siteId)); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable. | ||||
|      * | ||||
|      * @param {any} module Module. | ||||
|      * @param {number} courseId Course ID the module belongs to. | ||||
|      * @return {boolean|Promise<boolean>} Whether the module can be downloaded. The promise should never be rejected. | ||||
|      */ | ||||
|     isDownloadable(module: any, courseId: number): boolean | Promise<boolean> { | ||||
|         const siteId = this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lesson) => { | ||||
|             if (!this.lessonProvider.isLessonOffline(lesson)) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             // Check if there is any prevent access reason.
 | ||||
|             return this.lessonProvider.getAccessInformation(lesson.id, false, false, siteId).then((info) => { | ||||
|                 // It's downloadable if there are no prevent access reasons or there is just 1 and it's password.
 | ||||
|                 return !info.preventaccessreasons || !info.preventaccessreasons.length || | ||||
|                         (info.preventaccessreasons.length == 1 && this.lessonProvider.isPasswordProtected(info)); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return this.lessonProvider.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch a module. | ||||
|      * | ||||
|      * @param {any} module Module. | ||||
|      * @param {number} courseId Course ID the module belongs to. | ||||
|      * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. | ||||
|      * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> { | ||||
|         return this.prefetchPackage(module, courseId, single, this.prefetchLesson.bind(this)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch a lesson. | ||||
|      * | ||||
|      * @param {any} module Module. | ||||
|      * @param {number} courseId Course ID the module belongs to. | ||||
|      * @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section. | ||||
|      * @param {String} siteId Site ID. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected prefetchLesson(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { | ||||
|         let lesson, | ||||
|             password, | ||||
|             accessInfo; | ||||
| 
 | ||||
|         return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lessonData) => { | ||||
|             lesson = lessonData; | ||||
| 
 | ||||
|             // Get the lesson password if it's needed.
 | ||||
|             return this.getLessonPassword(lesson.id, false, true, single, siteId); | ||||
|         }).then((data) => { | ||||
|             password = data.password; | ||||
|             lesson = data.lesson || lesson; | ||||
|             accessInfo = data.accessInfo; | ||||
| 
 | ||||
|             if (!this.lessonProvider.leftDuringTimed(accessInfo)) { | ||||
|                 // The user didn't left during a timed session. Call launch retake to make sure there is a started retake.
 | ||||
|                 return this.lessonProvider.launchRetake(lesson.id, password, undefined, false, siteId).then(() => { | ||||
|                     const promises = []; | ||||
| 
 | ||||
|                     // New data generated, update the download time and refresh the access info.
 | ||||
|                     promises.push(this.filepoolProvider.updatePackageDownloadTime(siteId, this.component, module.id).catch(() => { | ||||
|                         // Ignore errors.
 | ||||
|                     })); | ||||
| 
 | ||||
|                     promises.push(this.lessonProvider.getAccessInformation(lesson.id, false, true, siteId).then((info) => { | ||||
|                         accessInfo = info; | ||||
|                     })); | ||||
| 
 | ||||
|                     return Promise.all(promises); | ||||
|                 }); | ||||
|             } | ||||
|         }).then(() => { | ||||
|             const promises = [], | ||||
|                 retake = accessInfo.attemptscount; | ||||
| 
 | ||||
|             // Download intro files and media files.
 | ||||
|             let files = lesson.mediafiles || []; | ||||
|             files = files.concat(this.getIntroFilesFromInstance(module, lesson)); | ||||
| 
 | ||||
|             promises.push(this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id)); | ||||
| 
 | ||||
|             // Get the list of pages.
 | ||||
|             promises.push(this.lessonProvider.getPages(lesson.id, password, false, true, siteId).then((pages) => { | ||||
|                 const subPromises = []; | ||||
|                 let hasRandomBranch = false; | ||||
| 
 | ||||
|                 // Get the data for each page.
 | ||||
|                 pages.forEach((data) => { | ||||
|                     // Check if any page has a RANDOMBRANCH jump.
 | ||||
|                     if (!hasRandomBranch) { | ||||
|                         for (let i = 0; i < data.jumps.length; i++) { | ||||
|                             if (data.jumps[i] == AddonModLessonProvider.LESSON_RANDOMBRANCH) { | ||||
|                                 hasRandomBranch = true; | ||||
|                                 break; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     // Get the page data. We don't pass accessInfo because we don't need to calculate the offline data.
 | ||||
|                     subPromises.push(this.lessonProvider.getPageData(lesson, data.page.id, password, false, true, false, | ||||
|                             true, undefined, undefined, siteId).then((pageData) => { | ||||
| 
 | ||||
|                         // Download the page files.
 | ||||
|                         let pageFiles = pageData.contentfiles || []; | ||||
| 
 | ||||
|                         pageData.answers.forEach((answer) => { | ||||
|                             if (answer.answerfiles && answer.answerfiles.length) { | ||||
|                                 pageFiles = pageFiles.concat(answer.answerfiles); | ||||
|                             } | ||||
|                             if (answer.responsefiles && answer.responsefiles.length) { | ||||
|                                 pageFiles = pageFiles.concat(answer.responsefiles); | ||||
|                             } | ||||
|                         }); | ||||
| 
 | ||||
|                         return this.filepoolProvider.addFilesToQueue(siteId, pageFiles, this.component, module.id); | ||||
|                     })); | ||||
|                 }); | ||||
| 
 | ||||
|                 // Prefetch the list of possible jumps for offline navigation. Do it here because we know hasRandomBranch.
 | ||||
|                 subPromises.push(this.lessonProvider.getPagesPossibleJumps(lesson.id, false, true, siteId).catch((error) => { | ||||
|                     if (hasRandomBranch) { | ||||
|                         // The WebSevice probably failed because RANDOMBRANCH aren't supported if the user hasn't seen any page.
 | ||||
|                         return Promise.reject(this.translate.instant('addon.mod_lesson.errorprefetchrandombranch')); | ||||
|                     } else { | ||||
|                         return Promise.reject(error); | ||||
|                     } | ||||
|                 })); | ||||
| 
 | ||||
|                 return Promise.all(subPromises); | ||||
|             })); | ||||
| 
 | ||||
|             // Prefetch user timers to be able to calculate timemodified in offline.
 | ||||
|             promises.push(this.lessonProvider.getTimers(lesson.id, false, true, siteId).catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             })); | ||||
| 
 | ||||
|             // Prefetch viewed pages in last retake to calculate progress.
 | ||||
|             promises.push(this.lessonProvider.getContentPagesViewedOnline(lesson.id, retake, false, true, siteId)); | ||||
| 
 | ||||
|             // Prefetch question attempts in last retake for offline calculations.
 | ||||
|             promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lesson.id, retake, false, undefined, false, true, siteId)); | ||||
| 
 | ||||
|             // Get module info to be able to handle links.
 | ||||
|             promises.push(this.courseProvider.getModuleBasicInfo(module.id, siteId)); | ||||
| 
 | ||||
|             if (accessInfo.canviewreports) { | ||||
|                 // Prefetch reports data.
 | ||||
|                 promises.push(this.groupsProvider.getActivityAllowedGroupsIfEnabled(module.id, undefined, siteId).then((groups) => { | ||||
|                     const subPromises = []; | ||||
| 
 | ||||
|                     groups.forEach((group) => { | ||||
|                         subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, group.id, false, true, siteId)); | ||||
|                     }); | ||||
| 
 | ||||
|                     // Always get group 0, even if there are no groups.
 | ||||
|                     subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, 0, false, true, siteId).then((data) => { | ||||
|                         if (!data || !data.students) { | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         // Prefetch the last retake for each user.
 | ||||
|                         const retakePromises = []; | ||||
| 
 | ||||
|                         data.students.forEach((student) => { | ||||
|                             if (!student.attempts || !student.attempts.length) { | ||||
|                                 return; | ||||
|                             } | ||||
| 
 | ||||
|                             const lastRetake = student.attempts[student.attempts.length - 1]; | ||||
|                             if (!lastRetake) { | ||||
|                                 return; | ||||
|                             } | ||||
| 
 | ||||
|                             retakePromises.push(this.lessonProvider.getUserRetake(lesson.id, lastRetake.try, student.id, false, | ||||
|                                     true, siteId)); | ||||
|                         }); | ||||
| 
 | ||||
|                         return Promise.all(retakePromises); | ||||
|                     })); | ||||
| 
 | ||||
|                     return Promise.all(subPromises); | ||||
|                 })); | ||||
|             } | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Validate the password. | ||||
|      * | ||||
|      * @param {number} lessonId Lesson ID. | ||||
|      * @param {any} info Lesson access info. | ||||
|      * @param {string} pwd Password to check. | ||||
|      * @param {boolean} [forceCache] Whether it should return cached data. Has priority over ignoreCache. | ||||
|      * @param {boolean} [ignoreCache] Whether 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<{password: string, lesson: any, accessInfo: any}>} Promise resolved when done. | ||||
|      */ | ||||
|     protected validatePassword(lessonId: number, info: any, pwd: string, forceCache?: boolean, ignoreCache?: boolean, | ||||
|             siteId?: string): Promise<{password: string, lesson: any, accessInfo: any}> { | ||||
| 
 | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         return this.lessonProvider.getLessonWithPassword(lessonId, pwd, true, forceCache, ignoreCache, siteId).then((lesson) => { | ||||
|             // Password is ok, store it and return the data.
 | ||||
|             return this.lessonProvider.storePassword(lesson.id, pwd, siteId).then(() => { | ||||
|                 return { | ||||
|                     password: pwd, | ||||
|                     lesson: lesson, | ||||
|                     accessInfo: info | ||||
|                 }; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										154
									
								
								src/addon/mod/lesson/providers/report-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/addon/mod/lesson/providers/report-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,154 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { NavController } from 'ionic-angular'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; | ||||
| import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | ||||
| import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseHelperProvider } from '@core/course/providers/helper'; | ||||
| import { AddonModLessonProvider } from './lesson'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to lesson report. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonReportLinkHandler extends CoreContentLinksHandlerBase { | ||||
|     name = 'AddonModLessonReportLinkHandler'; | ||||
|     featureName = 'CoreCourseModuleDelegate_AddonModLesson'; | ||||
|     pattern = /\/mod\/lesson\/report\.php.*([\&\?]id=\d+)/; | ||||
| 
 | ||||
|     constructor(protected domUtils: CoreDomUtilsProvider, protected lessonProvider: AddonModLessonProvider, | ||||
|             protected courseHelper: CoreCourseHelperProvider, protected linkHelper: CoreContentLinksHelperProvider, | ||||
|             protected courseProvider: CoreCourseProvider) { | ||||
|         super(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the list of actions for a link (url). | ||||
|      * | ||||
|      * @param {string[]} siteIds List of sites the URL belongs to. | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} [courseId] Course ID related to the URL. Optional but recommended. | ||||
|      * @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions. | ||||
|      */ | ||||
|     getActions(siteIds: string[], url: string, params: any, courseId?: number): | ||||
|             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { | ||||
|         courseId = courseId || params.courseid || params.cid; | ||||
| 
 | ||||
|         return [{ | ||||
|             action: (siteId, navCtrl?): void => { | ||||
|                 if (!params.action || params.action == 'reportoverview') { | ||||
|                     // Go to overview.
 | ||||
|                     this.openReportOverview(parseInt(params.id, 10), courseId, parseInt(params.group, 10), siteId, navCtrl); | ||||
|                 } else if (params.action == 'reportdetail') { | ||||
|                     this.openUserRetake(parseInt(params.id, 10), parseInt(params.userid, 10), courseId, parseInt(params.try, 10), | ||||
|                             siteId, navCtrl); | ||||
|                 } | ||||
|             } | ||||
|         }]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled for a certain site (site + user) and a URL. | ||||
|      * If not defined, defaults to true. | ||||
|      * | ||||
|      * @param {string} siteId The site ID. | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} [courseId] Course ID related to the URL. Optional but recommended. | ||||
|      * @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site. | ||||
|      */ | ||||
|     isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> { | ||||
|         if (params.action == 'reportdetail' && !params.userid) { | ||||
|             // Individual details are only available if the teacher is seeing a certain user.
 | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return this.lessonProvider.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open report overview. | ||||
|      * | ||||
|      * @param {number} moduleId Module ID. | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @param {string} groupId Group ID. | ||||
|      * @param {string} siteId Site ID. | ||||
|      * @param {NavController} [navCtrl] The NavController to use to navigate. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected openReportOverview(moduleId: number, courseId?: number, groupId?: number, siteId?: string, navCtrl?: NavController) | ||||
|             : Promise<any> { | ||||
| 
 | ||||
|         const modal = this.domUtils.showModalLoading(); | ||||
| 
 | ||||
|         // Get the module object.
 | ||||
|         return this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => { | ||||
|             courseId = courseId || module.course; | ||||
| 
 | ||||
|             const pageParams = { | ||||
|                 module: module, | ||||
|                 courseId: Number(courseId), | ||||
|                 action: 'report', | ||||
|                 group: groupId | ||||
|             }; | ||||
| 
 | ||||
|             this.linkHelper.goInSite(navCtrl, 'AddonModLessonIndexPage', pageParams, siteId); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error processing link.'); | ||||
|         }).finally(() => { | ||||
|             modal.dismiss(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open a user's retake. | ||||
|      * | ||||
|      * @param {number} moduleId Module ID. | ||||
|      * @param {number} userId User ID. | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @param {number} retake Retake to open. | ||||
|      * @param {string} groupId Group ID. | ||||
|      * @param {string} siteId Site ID. | ||||
|      * @param {NavController} [navCtrl] The NavController to use to navigate. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected openUserRetake(moduleId: number, userId: number, courseId: number, retake: number, siteId: string, | ||||
|             navCtrl?: NavController): Promise<any> { | ||||
| 
 | ||||
|         const modal = this.domUtils.showModalLoading(); | ||||
| 
 | ||||
|         // Get the module object.
 | ||||
|         return this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => { | ||||
|             courseId = courseId || module.course; | ||||
| 
 | ||||
|             const pageParams = { | ||||
|                 lessonId: module.instance, | ||||
|                 courseId: Number(courseId), | ||||
|                 userId: userId, | ||||
|                 retake: retake || 0 | ||||
|             }; | ||||
| 
 | ||||
|             this.linkHelper.goInSite(navCtrl, 'AddonModLessonUserRetakePage', pageParams, siteId); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error processing link.'); | ||||
|         }).finally(() => { | ||||
|             modal.dismiss(); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										47
									
								
								src/addon/mod/lesson/providers/sync-cron-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/addon/mod/lesson/providers/sync-cron-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { CoreCronHandler } from '@providers/cron'; | ||||
| import { AddonModLessonSyncProvider } from './lesson-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Synchronization cron handler. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModLessonSyncCronHandler implements CoreCronHandler { | ||||
|     name = 'AddonModLessonSyncCronHandler'; | ||||
| 
 | ||||
|     constructor(private lessonSync: AddonModLessonSyncProvider) {} | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the process. | ||||
|      * Receives the ID of the site affected, undefined for all sites. | ||||
|      * | ||||
|      * @param  {string} [siteId] ID of the site affected, undefined for all sites. | ||||
|      * @return {Promise<any>}         Promise resolved when done, rejected if failure. | ||||
|      */ | ||||
|     execute(siteId?: string): Promise<any> { | ||||
|         return this.lessonSync.syncAllLessons(siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the time between consecutive executions. | ||||
|      * | ||||
|      * @return {number} Time between consecutive executions (in ms). | ||||
|      */ | ||||
|     getInterval(): number { | ||||
|         return 600000; | ||||
|     } | ||||
| } | ||||
| @ -17,7 +17,7 @@ | ||||
| 
 | ||||
|     <!-- Warning message. --> | ||||
|     <div *ngIf="scorm && scorm.warningMessage" class="core-info-card" icon-start> | ||||
|         <ion-icon name="information"></ion-icon> | ||||
|         <ion-icon name="information-circle"></ion-icon> | ||||
|         {{ scorm.warningMessage }} | ||||
|     </div> | ||||
| 
 | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| 
 | ||||
|     <ion-item text-wrap [ngClass]="{invisible: !question.loaded}"> | ||||
|         <p *ngIf="!question.readOnly" class="core-info-card" icon-start> | ||||
|             <ion-icon name="information"></ion-icon> | ||||
|             <ion-icon name="information-circle"></ion-icon> | ||||
|             {{ 'core.question.howtodraganddrop' | translate }} | ||||
|         </p> | ||||
|         <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p> | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| 
 | ||||
|     <ion-item text-wrap [ngClass]="{invisible: !question.loaded}"> | ||||
|         <p *ngIf="!question.readOnly" class="core-info-card" icon-start> | ||||
|             <ion-icon name="information"></ion-icon> | ||||
|             <ion-icon name="information-circle"></ion-icon> | ||||
|             {{ 'core.question.howtodraganddrop' | translate }} | ||||
|         </p> | ||||
|         <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <section ion-list *ngIf="question.text || question.text === ''"> | ||||
|     <ion-item text-wrap class="addon-qtype-ddwtos-container"> | ||||
|         <p *ngIf="!question.readOnly" class="core-info-card" icon-start> | ||||
|             <ion-icon name="information"></ion-icon> | ||||
|             <ion-icon name="information-circle"></ion-icon> | ||||
|             {{ 'core.question.howtodraganddrop' | translate }} | ||||
|         </p> | ||||
|         <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p> | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
|                 <core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text> | ||||
|                 <p *ngIf="option.feedback" class="core-question-feedback-container"><core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"></core-format-text></p> | ||||
|             </ion-label> | ||||
|             <ion-checkbox [name]="option.name" [(ngModel)]="option.checked" [disabled]="option.disabled" item-end></ion-checkbox> | ||||
|             <ion-checkbox [attr.name]="option.name" [(ngModel)]="option.checked" [disabled]="option.disabled" item-end></ion-checkbox> | ||||
| 
 | ||||
|             <!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. --> | ||||
|             <input item-content type="hidden" [ngModel]="option.checked" [attr.name]="option.name"> | ||||
|  | ||||
| @ -88,6 +88,7 @@ import { AddonModFeedbackModule } from '@addon/mod/feedback/feedback.module'; | ||||
| import { AddonModFolderModule } from '@addon/mod/folder/folder.module'; | ||||
| import { AddonModForumModule } from '@addon/mod/forum/forum.module'; | ||||
| import { AddonModGlossaryModule } from '@addon/mod/glossary/glossary.module'; | ||||
| import { AddonModLessonModule } from '@addon/mod/lesson/lesson.module'; | ||||
| import { AddonModPageModule } from '@addon/mod/page/page.module'; | ||||
| import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module'; | ||||
| import { AddonModScormModule } from '@addon/mod/scorm/scorm.module'; | ||||
| @ -186,6 +187,7 @@ export const CORE_PROVIDERS: any[] = [ | ||||
|         AddonModChatModule, | ||||
|         AddonModChoiceModule, | ||||
|         AddonModLabelModule, | ||||
|         AddonModLessonModule, | ||||
|         AddonModResourceModule, | ||||
|         AddonModFeedbackModule, | ||||
|         AddonModFolderModule, | ||||
|  | ||||
| @ -11,7 +11,7 @@ core-show-password { | ||||
|         background: transparent; | ||||
|         padding: 0 ($content-padding / 2); | ||||
|         position: absolute; | ||||
|         top: $content-padding / 2; | ||||
|         bottom: $content-padding / 2; | ||||
|         right: 0; | ||||
|         margin-top: 0; | ||||
|         margin-bottom: 0; | ||||
| @ -25,25 +25,25 @@ core-show-password { | ||||
| 
 | ||||
| .md { | ||||
|     .item-label-stacked core-show-password .button[icon-only] { | ||||
|         top: 0; | ||||
|         bottom: 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .ios { | ||||
|     .item-label-stacked core-show-password .button[icon-only] { | ||||
|         top: -5px; | ||||
|         bottom: -5px; | ||||
|     } | ||||
|     core-show-password .button[icon-only] { | ||||
|         top: 0; | ||||
|         bottom: 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .wp { | ||||
|     .item-label-stacked core-show-password .button[icon-only] { | ||||
|         top: 7px; | ||||
|         bottom: 7px; | ||||
|     } | ||||
|     core-show-password .button[icon-only] { | ||||
|         top: 12px; | ||||
|         bottom: 12px; | ||||
|         right: 5px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -139,7 +139,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { | ||||
|         this.spinner = true; | ||||
| 
 | ||||
|         // Get download size to ask for confirm if it's high.
 | ||||
|         this.prefetchHandler.getDownloadSize(this.module, this.courseId).then((size) => { | ||||
|         this.prefetchHandler.getDownloadSize(this.module, this.courseId, true).then((size) => { | ||||
|             return this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh); | ||||
|         }).catch((error) => { | ||||
|             // Error, hide spinner.
 | ||||
|  | ||||
| @ -908,6 +908,10 @@ export class CoreCourseHelperProvider { | ||||
|         } | ||||
| 
 | ||||
|         return promise.then(() => { | ||||
|             // Make sure they're numbers.
 | ||||
|             courseId = Number(courseId); | ||||
|             sectionId = Number(sectionId); | ||||
| 
 | ||||
|             // Get the site.
 | ||||
|             return this.sitesProvider.getSite(siteId); | ||||
|         }).then((s) => { | ||||
|  | ||||
| @ -22,6 +22,12 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses'; | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreGradesProvider { | ||||
| 
 | ||||
|     static TYPE_NONE = 0; // Moodle's GRADE_TYPE_NONE.
 | ||||
|     static TYPE_VALUE = 1; // Moodle's GRADE_TYPE_VALUE.
 | ||||
|     static TYPE_SCALE = 2; // Moodle's GRADE_TYPE_SCALE.
 | ||||
|     static TYPE_TEXT = 3; // Moodle's GRADE_TYPE_TEXT.
 | ||||
| 
 | ||||
|     protected ROOT_CACHE_KEY = 'mmGrades:'; | ||||
| 
 | ||||
|     protected logger; | ||||
|  | ||||
| @ -33,7 +33,7 @@ | ||||
|             <form [formGroup]="credForm" (ngSubmit)="login()"> | ||||
|                 <ion-item> | ||||
|                     <core-show-password item-content [name]="'password'"> | ||||
|                         <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-show-password core-auto-focus></ion-input> | ||||
|                         <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-auto-focus></ion-input> | ||||
|                     </core-show-password> | ||||
|                 </ion-item> | ||||
|                 <ion-grid> | ||||
|  | ||||
| @ -17,6 +17,7 @@ | ||||
|     "disableall": "Disable notifications", | ||||
|     "disabled": "Disabled", | ||||
|     "displayformat": "Display format", | ||||
|     "enabledownloadsection": "Enable download sections", | ||||
|     "enablerichtexteditor": "Enable text editor", | ||||
|     "enablerichtexteditordescription": "If enabled, a text editor will be available when entering content.", | ||||
|     "enablesyncwifi": "Allow sync only when on Wi-Fi", | ||||
|  | ||||
| @ -131,6 +131,10 @@ export class CoreTextUtilsProvider { | ||||
|      * @return {string} Clean text. | ||||
|      */ | ||||
|     cleanTags(text: string, singleLine?: boolean): string { | ||||
|         if (typeof text == 'number') { | ||||
|             return text; | ||||
|         } | ||||
| 
 | ||||
|         if (!text) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user