MOBILE-3648 lesson: Implement user-retake page

main
Dani Palou 2021-02-03 15:59:12 +01:00
parent 90b3add5df
commit e04c19596f
11 changed files with 612 additions and 27 deletions

View File

@ -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>

View File

@ -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.
*/

View File

@ -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({

View File

@ -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');

View File

@ -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>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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.
};

View File

@ -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';

View File

@ -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.
};
/**

View File

@ -17,6 +17,8 @@
--white: #{$white};
--blue: #{$blue};
--blue-dark: #{$blue-dark};
--blue-light: #{$blue-light};
--turquoise: #{$turquoise};
--green: #{$green};
--red: #{$red};