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-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>
|
||||
<ion-label>
|
||||
|
|
|
@ -424,10 +424,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
pageId = continueLast ? this.accessInfo.lastpageseen : this.accessInfo.firstpageid;
|
||||
}
|
||||
|
||||
CoreNavigator.instance.navigate('../player', {
|
||||
await CoreNavigator.instance.navigate(`../player/${this.courseId}/${this.lesson.id}`, {
|
||||
params: {
|
||||
courseId: this.courseId,
|
||||
lessonId: this.lesson.id,
|
||||
pageId: pageId,
|
||||
password: this.password,
|
||||
},
|
||||
|
@ -474,10 +472,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
return;
|
||||
}
|
||||
|
||||
CoreNavigator.instance.navigate('../player', {
|
||||
CoreNavigator.instance.navigate(`../player/${this.courseId}/${this.lesson.id}`, {
|
||||
params: {
|
||||
courseId: this.courseId,
|
||||
lessonId: this.lesson.id,
|
||||
pageId: this.retakeToReview.pageid,
|
||||
password: this.password,
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -26,9 +26,13 @@ const routes: Routes = [
|
|||
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),
|
||||
},
|
||||
{
|
||||
path: 'user-retake/:courseId/:lessonId',
|
||||
loadChildren: () => import('./pages/user-retake/user-retake.module').then( m => m.AddonModLessonUserRetakePageModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -118,18 +118,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
|
|||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
const lessonId = CoreNavigator.instance.getRouteNumberParam('lessonId');
|
||||
const 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.lessonId = CoreNavigator.instance.getRouteNumberParam('lessonId')!;
|
||||
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
|
||||
this.password = CoreNavigator.instance.getRouteParam('password');
|
||||
this.review = !!CoreNavigator.instance.getRouteBooleanParam('review');
|
||||
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' })
|
||||
export class AddonModLessonModuleHandlerService implements CoreCourseModuleHandler {
|
||||
|
||||
static readonly PAGE_NAME = 'lesson';
|
||||
static readonly PAGE_NAME = 'mod_lesson';
|
||||
|
||||
name = 'AddonModLesson';
|
||||
modName = 'lesson';
|
||||
|
|
|
@ -4070,12 +4070,17 @@ export type AddonModLessonUserAttemptAnswerPageWSData = {
|
|||
contents: string; // Page contents.
|
||||
qtype: string; // Identifies the page type of this page.
|
||||
grayout: number; // If is required to apply a grayout.
|
||||
answerdata?: {
|
||||
score: string; // The score (text version).
|
||||
response: string; // The response text.
|
||||
responseformat: number; // Response. format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||
answers?: string[][]; // User answers.
|
||||
}; // Answer data (empty in content pages created in Moodle 1.x).
|
||||
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).
|
||||
response: string; // The response text.
|
||||
responseformat: number; // Response. format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||
answers?: string[][]; // User answers.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
--white: #{$white};
|
||||
|
||||
--blue: #{$blue};
|
||||
--blue-dark: #{$blue-dark};
|
||||
--blue-light: #{$blue-light};
|
||||
--turquoise: #{$turquoise};
|
||||
--green: #{$green};
|
||||
--red: #{$red};
|
||||
|
|
Loading…
Reference in New Issue