MOBILE-3648 lesson: Implement user-retake page
parent
90b3add5df
commit
e04c19596f
|
@ -300,7 +300,8 @@
|
||||||
<ion-card-subtitle>{{ 'addon.mod_lesson.overview' | translate }}</ion-card-subtitle>
|
<ion-card-subtitle>{{ 'addon.mod_lesson.overview' | translate }}</ion-card-subtitle>
|
||||||
</ion-card-header>
|
</ion-card-header>
|
||||||
|
|
||||||
<ion-item class="ion-text-wrap" *ngFor="let student of overview.students"> <!-- @todo navPush="AddonModLessonUserRetakePage" [navParams]="{courseId: courseId, lessonId: lesson.id, userId: student.id}" -->
|
<ion-item class="ion-text-wrap" *ngFor="let student of overview.students" button
|
||||||
|
(click)="openRetake(student.id)">
|
||||||
<core-user-avatar [user]="student" slot="start" [userId]="student.id" [courseId]="courseId">
|
<core-user-avatar [user]="student" slot="start" [userId]="student.id" [courseId]="courseId">
|
||||||
</core-user-avatar>
|
</core-user-avatar>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
|
|
@ -424,10 +424,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
pageId = continueLast ? this.accessInfo.lastpageseen : this.accessInfo.firstpageid;
|
pageId = continueLast ? this.accessInfo.lastpageseen : this.accessInfo.firstpageid;
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreNavigator.instance.navigate('../player', {
|
await CoreNavigator.instance.navigate(`../player/${this.courseId}/${this.lesson.id}`, {
|
||||||
params: {
|
params: {
|
||||||
courseId: this.courseId,
|
|
||||||
lessonId: this.lesson.id,
|
|
||||||
pageId: pageId,
|
pageId: pageId,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
},
|
},
|
||||||
|
@ -474,10 +472,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreNavigator.instance.navigate('../player', {
|
CoreNavigator.instance.navigate(`../player/${this.courseId}/${this.lesson.id}`, {
|
||||||
params: {
|
params: {
|
||||||
courseId: this.courseId,
|
|
||||||
lessonId: this.lesson.id,
|
|
||||||
pageId: this.retakeToReview.pageid,
|
pageId: this.retakeToReview.pageid,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
review: true,
|
review: true,
|
||||||
|
@ -692,6 +688,20 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a certain user retake.
|
||||||
|
*
|
||||||
|
* @param userId User ID to view.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async openRetake(userId: number): Promise<void> {
|
||||||
|
await CoreNavigator.instance.navigate(`../user-retake/${this.courseId}/${this.lesson!.id}`, {
|
||||||
|
params: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being destroyed.
|
* Component being destroyed.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -26,9 +26,13 @@ const routes: Routes = [
|
||||||
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModLessonIndexPageModule),
|
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModLessonIndexPageModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'player',
|
path: 'player/:courseId/:lessonId',
|
||||||
loadChildren: () => import('./pages/player/player.module').then( m => m.AddonModLessonPlayerPageModule),
|
loadChildren: () => import('./pages/player/player.module').then( m => m.AddonModLessonPlayerPageModule),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'user-retake/:courseId/:lessonId',
|
||||||
|
loadChildren: () => import('./pages/user-retake/user-retake.module').then( m => m.AddonModLessonUserRetakePageModule),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -118,18 +118,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
const lessonId = CoreNavigator.instance.getRouteNumberParam('lessonId');
|
this.lessonId = CoreNavigator.instance.getRouteNumberParam('lessonId')!;
|
||||||
const courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
|
||||||
if (!lessonId || !courseId) {
|
|
||||||
CoreDomUtils.instance.showErrorModal('No lesson ID or course ID supplied.');
|
|
||||||
this.forceLeave = true;
|
|
||||||
CoreNavigator.instance.back();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lessonId = lessonId;
|
|
||||||
this.courseId = courseId;
|
|
||||||
this.password = CoreNavigator.instance.getRouteParam('password');
|
this.password = CoreNavigator.instance.getRouteParam('password');
|
||||||
this.review = !!CoreNavigator.instance.getRouteBooleanParam('review');
|
this.review = !!CoreNavigator.instance.getRouteBooleanParam('review');
|
||||||
this.currentPage = CoreNavigator.instance.getRouteNumberParam('pageId');
|
this.currentPage = CoreNavigator.instance.getRouteNumberParam('pageId');
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>{{ 'addon.mod_lesson.detailedstats' | translate }}</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<div *ngIf="student">
|
||||||
|
<!-- Student data. -->
|
||||||
|
<ion-item class="ion-text-wrap" core-user-link [userId]="student.id" [courseId]="courseId" [title]="student.fullname">
|
||||||
|
<core-user-avatar [user]="student" slot="start" [userId]="student.id" [courseId]="courseId">
|
||||||
|
</core-user-avatar>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{student.fullname}}</h2>
|
||||||
|
<core-progress-bar [progress]="student.bestgrade"></core-progress-bar>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Retake selector if there is more than one retake. -->
|
||||||
|
<ion-item class="ion-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="action-sheet">
|
||||||
|
<ion-select-option *ngFor="let retake of student.attempts" [value]="retake.try">
|
||||||
|
{{retake.label}}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Retake stats. -->
|
||||||
|
<ion-list *ngIf="retake && retake.userstats && retake.userstats.gradeinfo" class="addon-mod_lesson-userstats">
|
||||||
|
<ion-grid class="ion-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-grid>
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_lesson.timetaken' | translate }}</p>
|
||||||
|
<p>{{ timeTakenReadable }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_lesson.completed' | translate }}</p>
|
||||||
|
<p>{{ retake.userstats.completed * 1000 | coreFormatDate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<!-- Not completed, no stats. -->
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="retake && (!retake.userstats || !retake.userstats.gradeinfo)">
|
||||||
|
<ion-label>{{ 'addon.mod_lesson.notcompleted' | translate }}</ion-label>
|
||||||
|
</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 class="ion-text-wrap">
|
||||||
|
<ion-card-subtitle>{{page.qtype}}: {{page.title}}</ion-card-subtitle>
|
||||||
|
</ion-card-header>
|
||||||
|
<ion-item class="ion-text-wrap" lines="none">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_lesson.question' | translate }}</p>
|
||||||
|
<p>
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule" [maxHeight]="50"
|
||||||
|
[text]="page.contents" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" lines="none">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_lesson.answer' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" lines="none"
|
||||||
|
*ngIf="!page.answerdata || !page.answerdata.answers || !page.answerdata.answers.length">
|
||||||
|
<ion-label>
|
||||||
|
<p>{{ 'addon.mod_lesson.didnotanswerquestion' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</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-grid class="ion-text-wrap" *ngIf="page.isContent">
|
||||||
|
<!-- Content page, display a button and the content. -->
|
||||||
|
<ion-row>
|
||||||
|
<ion-col>
|
||||||
|
<ion-button expand="block" class="ion-text-wrap" color="light" [disabled]="true">{{ answer[0].buttonText }}</ion-button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<p [innerHTML]="answer[0].content"></p>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
|
||||||
|
<div *ngIf="page.isQuestion">
|
||||||
|
<!-- Question page, show the right input for the answer. -->
|
||||||
|
|
||||||
|
<!-- Truefalse or matching. -->
|
||||||
|
<ion-item class="ion-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" contextLevel="module"
|
||||||
|
[contextInstanceId]="lesson?.coursemodule" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
<ion-badge *ngIf="answer[1]" color="dark">
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
|
||||||
|
[text]="answer[1]" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-badge>
|
||||||
|
</ion-label>
|
||||||
|
<ion-checkbox [attr.name]="answer[0].name" [ngModel]="answer[0].checked" [disabled]="true"
|
||||||
|
slot="end">
|
||||||
|
</ion-checkbox>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Short answer or numeric. -->
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="answer[0].isText" lines="none">
|
||||||
|
<ion-label>
|
||||||
|
<p>{{ answer[0].value }}</p>
|
||||||
|
<ion-badge *ngIf="answer[1]" color="dark">
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
|
||||||
|
[text]="answer[1]" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-badge>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Matching. -->
|
||||||
|
<ion-grid class="ion-text-wrap" *ngIf="answer[0].isSelect">
|
||||||
|
<ion-row>
|
||||||
|
<ion-col>
|
||||||
|
<p>
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
|
||||||
|
[text]=" answer[0].content" contextLevel="module"
|
||||||
|
[contextInstanceId]="lesson?.coursemodule" [courseId]="courseId">
|
||||||
|
</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]" contextLevel="module"
|
||||||
|
[contextInstanceId]="lesson?.coursemodule" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-badge>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
|
||||||
|
<!-- Essay or couldn't determine. -->
|
||||||
|
<ion-item class="ion-text-wrap" lines="none"
|
||||||
|
*ngIf="!answer[0].isCheckbox && !answer[0].isText && !answer[0].isSelect">
|
||||||
|
<ion-label>
|
||||||
|
<p>
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
|
||||||
|
[text]="answer[0]" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
<ion-badge *ngIf="answer[1]" color="dark">
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
|
||||||
|
[text]="answer[1]" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-badge>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="!page.isContent && !page.isQuestion" lines="none">
|
||||||
|
<!-- Another page (end of branch, ...). -->
|
||||||
|
<ion-label>
|
||||||
|
<p>
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
|
||||||
|
[text]="answer[0]" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
<ion-badge *ngIf="answer[1]" color="dark">
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
|
||||||
|
[text]="answer[1]" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-badge>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="page.answerdata.response" lines="none">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_lesson.response' | translate }}</p>
|
||||||
|
<p>
|
||||||
|
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
|
||||||
|
[text]="page.answerdata.response" contextLevel="module"
|
||||||
|
[contextInstanceId]="lesson?.coursemodule" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="page.answerdata.score">
|
||||||
|
<ion-label><p>{{page.answerdata.score}}</p></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
</ion-card>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,46 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// 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 { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { AddonModLessonUserRetakePage } from './user-retake';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AddonModLessonUserRetakePage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
FormsModule,
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AddonModLessonUserRetakePage,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class AddonModLessonUserRetakePageModule {}
|
|
@ -0,0 +1,17 @@
|
||||||
|
:host {
|
||||||
|
.button-disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-mod_lesson-highlight {
|
||||||
|
--background: var(--blue-light);
|
||||||
|
|
||||||
|
ion-label, ion-label p {
|
||||||
|
color: var(--blue-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-interactive-disabled ion-label {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,275 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// 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 { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import {
|
||||||
|
AddonModLesson,
|
||||||
|
AddonModLessonAttemptsOverviewsAttemptWSData,
|
||||||
|
AddonModLessonAttemptsOverviewsStudentWSData,
|
||||||
|
AddonModLessonGetUserAttemptWSResponse,
|
||||||
|
AddonModLessonLessonWSData,
|
||||||
|
AddonModLessonProvider,
|
||||||
|
AddonModLessonUserAttemptAnswerData,
|
||||||
|
AddonModLessonUserAttemptAnswerPageWSData,
|
||||||
|
} from '../../services/lesson';
|
||||||
|
import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a retake made by a certain user.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-lesson-user-retake',
|
||||||
|
templateUrl: 'user-retake.html',
|
||||||
|
styleUrls: ['user-retake.scss'],
|
||||||
|
})
|
||||||
|
export class AddonModLessonUserRetakePage implements OnInit {
|
||||||
|
|
||||||
|
component = AddonModLessonProvider.COMPONENT;
|
||||||
|
lesson?: AddonModLessonLessonWSData; // The lesson the retake belongs to.
|
||||||
|
courseId!: number; // Course ID the lesson belongs to.
|
||||||
|
selectedRetake?: number; // The retake to see.
|
||||||
|
student?: StudentData; // Data about the student and his retakes.
|
||||||
|
retake?: RetakeToDisplay; // Data about the retake.
|
||||||
|
loaded?: boolean; // Whether the data has been loaded.
|
||||||
|
timeTakenReadable?: string; // Time taken in a readable format.
|
||||||
|
|
||||||
|
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.
|
||||||
|
protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.lessonId = CoreNavigator.instance.getRouteNumberParam('lessonId')!;
|
||||||
|
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
|
||||||
|
this.userId = CoreNavigator.instance.getRouteNumberParam('userId') || CoreSites.instance.getCurrentSiteUserId();
|
||||||
|
this.retakeNumber = CoreNavigator.instance.getRouteNumberParam('retake');
|
||||||
|
|
||||||
|
// Fetch the data.
|
||||||
|
this.fetchData().finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the retake displayed.
|
||||||
|
*
|
||||||
|
* @param retakeNumber The new retake number.
|
||||||
|
*/
|
||||||
|
async changeRetake(retakeNumber: number): Promise<void> {
|
||||||
|
this.loaded = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.setRetake(retakeNumber);
|
||||||
|
} catch (error) {
|
||||||
|
this.selectedRetake = this.previousSelectedRetake;
|
||||||
|
CoreDomUtils.instance.showErrorModal(CoreUtils.instance.addDataNotDownloadedError(error, 'Error getting attempt.'));
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull to refresh.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
*/
|
||||||
|
doRefresh(refresher: CustomEvent<IonRefresher>): void {
|
||||||
|
this.refreshData().finally(() => {
|
||||||
|
refresher?.detail.complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get lesson and retake data.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchData(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.lesson = await AddonModLesson.instance.getLessonById(this.courseId, this.lessonId);
|
||||||
|
|
||||||
|
// Get the retakes overview for all participants.
|
||||||
|
const data = await AddonModLesson.instance.getRetakesOverview(this.lesson.id, {
|
||||||
|
cmId: this.lesson.coursemodule,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search the student.
|
||||||
|
const student: StudentData | undefined = data?.students?.find(student => student.id == this.userId);
|
||||||
|
if (!student) {
|
||||||
|
// Student not found.
|
||||||
|
throw new CoreError(Translate.instance.instant('addon.mod_lesson.cannotfinduser'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!student.attempts.length) {
|
||||||
|
// No retakes.
|
||||||
|
throw new CoreError(Translate.instance.instant('addon.mod_lesson.cannotfindattempt'));
|
||||||
|
}
|
||||||
|
|
||||||
|
student.bestgrade = CoreTextUtils.instance.roundToDecimals(student.bestgrade, 2);
|
||||||
|
student.attempts.forEach((retake) => {
|
||||||
|
if (!this.selectedRetake && this.retakeNumber == retake.try) {
|
||||||
|
// The retake specified as parameter exists. Use it.
|
||||||
|
this.selectedRetake = this.retakeNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
retake.label = AddonModLessonHelper.instance.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.
|
||||||
|
const user = await CoreUtils.instance.ignoreErrors(CoreUser.instance.getProfile(student.id, this.courseId, true));
|
||||||
|
|
||||||
|
this.student = student;
|
||||||
|
this.student.profileimageurl = user?.profileimageurl;
|
||||||
|
|
||||||
|
await this.setRetake(this.selectedRetake);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting data.', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes data.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async refreshData(): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
promises.push(AddonModLesson.instance.invalidateLessonData(this.courseId));
|
||||||
|
if (this.lesson) {
|
||||||
|
promises.push(AddonModLesson.instance.invalidateRetakesOverview(this.lesson.id));
|
||||||
|
promises.push(AddonModLesson.instance.invalidateUserRetakesForUser(this.lesson.id, this.userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUtils.instance.ignoreErrors(Promise.all(promises));
|
||||||
|
|
||||||
|
await this.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the retake to view and load its data.
|
||||||
|
*
|
||||||
|
* @param retakeNumber Retake number to set.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async setRetake(retakeNumber: number): Promise<void> {
|
||||||
|
this.selectedRetake = retakeNumber;
|
||||||
|
|
||||||
|
const retakeData = await AddonModLesson.instance.getUserRetake(this.lessonId, retakeNumber, {
|
||||||
|
cmId: this.lesson!.coursemodule,
|
||||||
|
userId: this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.retake = this.formatRetake(retakeData);
|
||||||
|
this.previousSelectedRetake = this.selectedRetake;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format retake data, adding some calculated data.
|
||||||
|
*
|
||||||
|
* @param data Retake data.
|
||||||
|
* @return Formatted data.
|
||||||
|
*/
|
||||||
|
protected formatRetake(retakeData: AddonModLessonGetUserAttemptWSResponse): RetakeToDisplay {
|
||||||
|
const formattedData = <RetakeToDisplay> retakeData;
|
||||||
|
|
||||||
|
if (formattedData.userstats.gradeinfo) {
|
||||||
|
// Completed.
|
||||||
|
formattedData.userstats.grade = CoreTextUtils.instance.roundToDecimals(formattedData.userstats.grade, 2);
|
||||||
|
this.timeTakenReadable = CoreTimeUtils.instance.formatTime(formattedData.userstats.timetotake);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format pages data.
|
||||||
|
formattedData.answerpages.forEach((page) => {
|
||||||
|
if (AddonModLesson.instance.answerPageIsContent(page)) {
|
||||||
|
page.isContent = true;
|
||||||
|
|
||||||
|
if (page.answerdata?.answers) {
|
||||||
|
page.answerdata.answers.forEach((answer) => {
|
||||||
|
// Content pages only have 1 valid field in the answer array.
|
||||||
|
answer[0] = AddonModLessonHelper.instance.getContentPageAnswerDataFromHtml(answer[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (AddonModLesson.instance.answerPageIsQuestion(page)) {
|
||||||
|
page.isQuestion = true;
|
||||||
|
|
||||||
|
if (page.answerdata?.answers) {
|
||||||
|
page.answerdata.answers.forEach((answer) => {
|
||||||
|
// Only the first field of the answer array requires to be parsed.
|
||||||
|
answer[0] = AddonModLessonHelper.instance.getQuestionPageAnswerDataFromHtml(answer[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Student data with some calculated data.
|
||||||
|
*/
|
||||||
|
type StudentData = Omit<AddonModLessonAttemptsOverviewsStudentWSData, 'attempts'> & {
|
||||||
|
profileimageurl?: string;
|
||||||
|
attempts: AttemptWithLabel[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Student attempt with a calculated label.
|
||||||
|
*/
|
||||||
|
type AttemptWithLabel = AddonModLessonAttemptsOverviewsAttemptWSData & {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Retake with calculated data.
|
||||||
|
*/
|
||||||
|
type RetakeToDisplay = Omit<AddonModLessonGetUserAttemptWSResponse, 'answerpages'> & {
|
||||||
|
answerpages: AnswerPage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answer page with calculated data.
|
||||||
|
*/
|
||||||
|
type AnswerPage = Omit<AddonModLessonUserAttemptAnswerPageWSData, 'answerdata'> & {
|
||||||
|
isContent?: boolean;
|
||||||
|
isQuestion?: boolean;
|
||||||
|
answerdata?: AnswerData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answer data with calculated data.
|
||||||
|
*/
|
||||||
|
type AnswerData = Omit<AddonModLessonUserAttemptAnswerData, 'answers'> & {
|
||||||
|
answers?: (string[] | AddonModLessonAnswerData)[]; // User answers.
|
||||||
|
};
|
|
@ -30,7 +30,7 @@ import { makeSingleton } from '@singletons';
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AddonModLessonModuleHandlerService implements CoreCourseModuleHandler {
|
export class AddonModLessonModuleHandlerService implements CoreCourseModuleHandler {
|
||||||
|
|
||||||
static readonly PAGE_NAME = 'lesson';
|
static readonly PAGE_NAME = 'mod_lesson';
|
||||||
|
|
||||||
name = 'AddonModLesson';
|
name = 'AddonModLesson';
|
||||||
modName = 'lesson';
|
modName = 'lesson';
|
||||||
|
|
|
@ -4070,12 +4070,17 @@ export type AddonModLessonUserAttemptAnswerPageWSData = {
|
||||||
contents: string; // Page contents.
|
contents: string; // Page contents.
|
||||||
qtype: string; // Identifies the page type of this page.
|
qtype: string; // Identifies the page type of this page.
|
||||||
grayout: number; // If is required to apply a grayout.
|
grayout: number; // If is required to apply a grayout.
|
||||||
answerdata?: {
|
answerdata?: AddonModLessonUserAttemptAnswerData; // Answer data (empty in content pages created in Moodle 1.x).
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answer data of a user attempt answer page.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonUserAttemptAnswerData = {
|
||||||
score: string; // The score (text version).
|
score: string; // The score (text version).
|
||||||
response: string; // The response text.
|
response: string; // The response text.
|
||||||
responseformat: number; // Response. format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
responseformat: number; // Response. format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||||
answers?: string[][]; // User answers.
|
answers?: string[][]; // User answers.
|
||||||
}; // Answer data (empty in content pages created in Moodle 1.x).
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
--white: #{$white};
|
--white: #{$white};
|
||||||
|
|
||||||
--blue: #{$blue};
|
--blue: #{$blue};
|
||||||
|
--blue-dark: #{$blue-dark};
|
||||||
|
--blue-light: #{$blue-light};
|
||||||
--turquoise: #{$turquoise};
|
--turquoise: #{$turquoise};
|
||||||
--green: #{$green};
|
--green: #{$green};
|
||||||
--red: #{$red};
|
--red: #{$red};
|
||||||
|
|
Loading…
Reference in New Issue