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>
|
||||
|
||||
|
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
addon-mod-lesson-index {
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -0,0 +1,5 @@
|
|||
page-addon-mod-lesson-menu-modal {
|
||||
.addon-mod_lesson-selected, .item.addon-mod_lesson-selected {
|
||||
background: $blue-light;
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -0,0 +1,8 @@
|
|||
page-addon-mod-lesson-user-retake {
|
||||
.addon-mod_lesson-highlight {
|
||||
background: $blue-light;
|
||||
.label, .label p {
|
||||
color: $blue-dark;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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…
Reference in New Issue