forked from CIT/Vmeda.Online
		
	MOBILE-3645 h5pactivity: Implement reports pages
This commit is contained in:
		
							parent
							
								
									612bc6bffa
								
							
						
					
					
						commit
						a3924cbbcb
					
				@ -26,6 +26,16 @@ const routes: Routes = [
 | 
			
		||||
        component: AddonModH5PActivityIndexPage,
 | 
			
		||||
        canDeactivate: [CanLeaveGuard],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/userattempts/:userId',
 | 
			
		||||
        loadChildren: () => import('./pages/user-attempts/user-attempts.module')
 | 
			
		||||
            .then( m => m.AddonModH5PActivityUserAttemptsPageModule),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/attemptresults/:attemptId',
 | 
			
		||||
        loadChildren: () => import('./pages/attempt-results/attempt-results.module')
 | 
			
		||||
            .then( m => m.AddonModH5PActivityAttemptResultsPageModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,193 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text *ngIf="h5pActivity" [text]="h5pActivity.name" contextLevel="module"
 | 
			
		||||
                [contextInstanceId]="h5pActivity.coursemodule" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <ng-container *ngIf="attempt">
 | 
			
		||||
            <!-- Attempt number and user that did the attempt. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="user" core-user-link [userId]="user.id" [courseId]="courseId"
 | 
			
		||||
                [title]="user.fullname">
 | 
			
		||||
                <core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2>{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}: {{user.fullname}}</h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <!-- Attempt number (if user not known). -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="!user">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2>{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}</h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Attempt summary. -->
 | 
			
		||||
            <ion-card class="addon-mod_h5pactivity-attempt-result-summary">
 | 
			
		||||
                <ion-list>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.startdate' | translate }}</h2>
 | 
			
		||||
                            <p>{{ attempt.timecreated | coreFormatDate:'strftimedatetime' }}</p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.completion' | translate }}</h2>
 | 
			
		||||
                            <p *ngIf="attempt.completion">
 | 
			
		||||
                                <img src="assets/img/completion/completion-auto-y.svg" role="presentation" alt="">
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_completion_yes' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="!attempt.completion">
 | 
			
		||||
                                <img src="assets/img/completion/completion-auto-n.svg" role="presentation" alt="">
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_completion_no' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.duration' | translate }}</h2>
 | 
			
		||||
                            <p>{{ attempt.durationReadable }}</p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.outcome' | translate }}</h2>
 | 
			
		||||
                            <p *ngIf="attempt.success !== null && attempt.success" >
 | 
			
		||||
                                <ion-icon name="fa-check-circle"></ion-icon>
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_success_pass' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="attempt.success !== null && !attempt.success" >
 | 
			
		||||
                                <ion-icon name="far-circle"></ion-icon>
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_success_fail' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="attempt.success === null" >
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_success_unknown' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item *ngIf="attempt.maxscore" class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.totalscore' | translate }}</h2>
 | 
			
		||||
                            <p>{{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: attempt} }}</p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ion-list>
 | 
			
		||||
            </ion-card>
 | 
			
		||||
 | 
			
		||||
            <!-- Results. -->
 | 
			
		||||
            <ng-container *ngIf="attempt.results">
 | 
			
		||||
                <ion-card *ngFor="let result of attempt.results">
 | 
			
		||||
                    <ion-card-header class="ion-text-wrap">
 | 
			
		||||
                        <ion-card-title>
 | 
			
		||||
                            <core-format-text [text]="result.description" [component]="component" [componentId]="cmId"
 | 
			
		||||
                                contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </ion-card-title>
 | 
			
		||||
                    </ion-card-header>
 | 
			
		||||
                    <ion-item *ngIf="result.content" class="ion-text-wrap">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <core-format-text [text]="result.content" [component]="component" [componentId]="cmId"
 | 
			
		||||
                                contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
 | 
			
		||||
                    <!-- Options. -->
 | 
			
		||||
                    <ng-container *ngIf="result.options && result.options.length">
 | 
			
		||||
                        <ion-item class="ion-text-wrap addon-mod_h5pactivity-result-table-header">
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <ion-row class="ion-align-items-center">
 | 
			
		||||
                                    <ion-col class="ion-text-center">{{ result.optionslabel }}</ion-col>
 | 
			
		||||
                                    <ion-col class="ion-text-center">{{ result.correctlabel }}</ion-col>
 | 
			
		||||
                                    <ion-col class="ion-text-center">{{ result.answerlabel }}</ion-col>
 | 
			
		||||
                                </ion-row>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                        <ion-item *ngFor="let option of result.options"
 | 
			
		||||
                            class="ion-text-wrap addon-mod_h5pactivity-result-table-row">
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <ion-row class="ion-align-items-center">
 | 
			
		||||
                                    <ion-col class="ion-text-center">
 | 
			
		||||
                                        <core-format-text [text]="option.description" [component]="component" [componentId]="cmId"
 | 
			
		||||
                                            contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
 | 
			
		||||
                                        </core-format-text>
 | 
			
		||||
                                    </ion-col>
 | 
			
		||||
                                    <ion-col class="ion-text-center">
 | 
			
		||||
                                        <ng-container *ngIf="option.correctanswer">
 | 
			
		||||
                                            <ng-container
 | 
			
		||||
                                                *ngTemplateOutlet="answerTemplate; context: {answer: option.correctanswer}">
 | 
			
		||||
                                            </ng-container>
 | 
			
		||||
                                        </ng-container>
 | 
			
		||||
                                    </ion-col>
 | 
			
		||||
                                    <ion-col class="ion-text-center">
 | 
			
		||||
                                        <ng-container *ngIf="option.useranswer">
 | 
			
		||||
                                            <ng-container *ngTemplateOutlet="answerTemplate; context: {answer: option.useranswer}">
 | 
			
		||||
                                            </ng-container>
 | 
			
		||||
                                        </ng-container>
 | 
			
		||||
                                    </ion-col>
 | 
			
		||||
                                </ion-row>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
 | 
			
		||||
                        <!-- Result score. -->
 | 
			
		||||
                        <ion-item *ngIf="result.maxscore" class="ion-text-wrap ion-text-end addon-mod_h5pactivity-result-score">
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <p><strong>
 | 
			
		||||
                                    {{ 'addon.mod_h5pactivity.score' | translate }}:
 | 
			
		||||
                                     {{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: result} }}
 | 
			
		||||
                                </strong></p>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
 | 
			
		||||
                    <!-- Result doesn't support tracking. -->
 | 
			
		||||
                    <ion-item class="ion-text-wrap core-warning-item" *ngIf="!result.track" lines="none">
 | 
			
		||||
                        <ion-icon slot="start" name="fas-exclamation-triangle" color="warning"></ion-icon>
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            {{ 'addon.mod_h5pactivity.no_compatible_track' | translate:{$a: result.interactiontype} }}
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ion-card>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
<!-- Template to render an answer. -->
 | 
			
		||||
<ng-template #answerTemplate let-answer="answer">
 | 
			
		||||
    <p *ngIf="answer.correct">
 | 
			
		||||
        <ion-icon name="fa-check" [attr.aria-label]="'addon.mod_h5pactivity.answer_correct' | translate" color="success">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
        {{ answer.answer }}
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.incorrect">
 | 
			
		||||
        <ion-icon name="fa-remove" [attr.aria-label]="'addon.mod_h5pactivity.answer_incorrect' | translate" color="danger">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
        {{ answer.answer }}
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.text">
 | 
			
		||||
        {{ answer.answer }}
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.checked">
 | 
			
		||||
        <ion-icon name="fa-check-circle" [attr.aria-label]="'addon.mod_h5pactivity.answer_checked' | translate">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.pass">
 | 
			
		||||
        <ion-icon name="fa-check" [attr.aria-label]="'addon.mod_h5pactivity.answer_pass' | translate" color="success">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.fail">
 | 
			
		||||
        <ion-icon name="fa-remove" [attr.aria-label]="'addon.mod_h5pactivity.answer_fail' | translate" color="danger">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
    </p>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
// (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 { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { AddonModH5PActivityAttemptResultsPage } from './attempt-results';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonModH5PActivityAttemptResultsPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModH5PActivityAttemptResultsPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityAttemptResultsPageModule {}
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    .core-warning-item {
 | 
			
		||||
        --inner-border-width: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-attempt-result-summary {
 | 
			
		||||
        img {
 | 
			
		||||
            width: 16px;
 | 
			
		||||
            height: 16px;
 | 
			
		||||
            display: inline;
 | 
			
		||||
            @include margin-horizontal(0, 4px);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-attempt-result-summary,
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-header,
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-row {
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            font-size: 1.2em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-header {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-row.item:nth-child(even) {
 | 
			
		||||
        --background: var(--gray-lighter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-result-score {
 | 
			
		||||
        border-top: 1px solid black;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(body.dark) {
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-row.item:nth-child(even) {
 | 
			
		||||
        --background: var(--black);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,124 @@
 | 
			
		||||
// (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 { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModH5PActivity,
 | 
			
		||||
    AddonModH5PActivityProvider,
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityAttemptResults,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays results of an attempt.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-h5pactivity-attempt-results',
 | 
			
		||||
    templateUrl: 'attempt-results.html',
 | 
			
		||||
    styleUrls: ['attempt-results.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityAttemptResultsPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    h5pActivity?: AddonModH5PActivityData;
 | 
			
		||||
    attempt?: AddonModH5PActivityAttemptResults;
 | 
			
		||||
    user?: CoreUserProfile;
 | 
			
		||||
    component = AddonModH5PActivityProvider.COMPONENT;
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    cmId!: number;
 | 
			
		||||
 | 
			
		||||
    protected attemptId!: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
 | 
			
		||||
        this.attemptId = CoreNavigator.getRouteNumberParam('attemptId')!;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     */
 | 
			
		||||
    doRefresh(refresher: IonRefresher): void {
 | 
			
		||||
        this.refreshData().finally(() => {
 | 
			
		||||
            refresher.complete();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get quiz data and attempt data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
 | 
			
		||||
        this.attempt = await AddonModH5PActivity.getAttemptResults(this.h5pActivity.id, this.attemptId, {
 | 
			
		||||
            cmId: this.cmId,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await this.fetchUserProfile();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get user profile.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchUserProfile(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            this.user = await CoreUser.getProfile(this.attempt!.userid, this.courseId, true);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async refreshData(): Promise<void> {
 | 
			
		||||
        const promises = [
 | 
			
		||||
            AddonModH5PActivity.invalidateActivityData(this.courseId),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (this.h5pActivity) {
 | 
			
		||||
            promises.push(AddonModH5PActivity.invalidateAttemptResults(this.h5pActivity.id, this.attemptId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.ignoreErrors(Promise.all(promises));
 | 
			
		||||
 | 
			
		||||
        await this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,114 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text *ngIf="h5pActivity" [text]="h5pActivity.name" contextLevel="module"
 | 
			
		||||
                [contextInstanceId]="h5pActivity.coursemodule" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <!-- User viewed. -->
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="user && !isCurrentUser" core-user-link [userId]="user.id" [courseId]="courseId"
 | 
			
		||||
            [title]="user.fullname">
 | 
			
		||||
            <core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <h2>{{ user.fullname }}</h2>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="user && isCurrentUser">
 | 
			
		||||
            <core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <h2>{{ 'addon.mod_h5pactivity.myattempts' | translate }}</h2>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <ion-list *ngIf="attemptsData">
 | 
			
		||||
            <!-- Attempts used to calculate the score. -->
 | 
			
		||||
            <ng-container *ngIf="attemptsData.scored">
 | 
			
		||||
                <ion-item-divider>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ attemptsData.scored.title }}</h2>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item-divider>
 | 
			
		||||
                <ng-container *ngTemplateOutlet="attemptsTemplate; context: {attempts: attemptsData.scored.attempts}">
 | 
			
		||||
                </ng-container>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
            <!-- All attempts. -->
 | 
			
		||||
            <ng-container *ngIf="attemptsData.attempts && attemptsData.attempts.length">
 | 
			
		||||
                <ion-item-divider>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_h5pactivity.all_attempts' | translate }}</h2>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item-divider>
 | 
			
		||||
                <ng-container *ngTemplateOutlet="attemptsTemplate; context: {attempts: attemptsData.attempts}"></ng-container>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
 | 
			
		||||
        <!-- No attempts. -->
 | 
			
		||||
        <core-empty-box *ngIf="attemptsData && (!attemptsData.attempts || !attemptsData.attempts.length)" icon="stats-chart"
 | 
			
		||||
            [message]="'addon.mod_h5pactivity.attempts_none' | translate">
 | 
			
		||||
        </core-empty-box>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
<!-- Template to render a list of conversations. -->
 | 
			
		||||
<ng-template #attemptsTemplate let-attempts="attempts">
 | 
			
		||||
    <!-- "Header" of the table -->
 | 
			
		||||
    <ion-item class="ion-text-wrap addon-mod_h5pactivity-table-header" detail="true">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <ion-row class="ion-align-items-center">
 | 
			
		||||
                <ion-col class="ion-text-center">#</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center" size="5" size-md="2">{{ 'core.date' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center">{{ 'addon.mod_h5pactivity.score' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ 'addon.mod_h5pactivity.maxscore' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ 'addon.mod_h5pactivity.duration' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ 'addon.mod_h5pactivity.completion' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center">{{ 'core.success' | translate }}</ion-col>
 | 
			
		||||
            </ion-row>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
 | 
			
		||||
    <!-- List of attempts. -->
 | 
			
		||||
    <ion-item class="ion-text-wrap addon-mod_h5pactivity-table-row" *ngFor="let attempt of attempts" button detail="true"
 | 
			
		||||
        [attr.aria-label]="'addon.mod_h5pactivity.viewattempt' | translate:{$a: attempt.attempt}" (click)="openAttempt(attempt)">
 | 
			
		||||
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <ion-row class="ion-align-items-center">
 | 
			
		||||
                <ion-col class="ion-text-center">{{ attempt.attempt }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center" size="5" size-md="2">
 | 
			
		||||
                    {{ attempt.timemodified | coreFormatDate:'strftimedatetimeshort' }}
 | 
			
		||||
                </ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center">
 | 
			
		||||
                    {{ attempt.rawscore }}<span class="ion-hide-md-up"> / {{ attempt.maxscore }}</span>
 | 
			
		||||
                </ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ attempt.maxscore }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ attempt.durationReadable }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">
 | 
			
		||||
                    <img *ngIf="attempt.completion" src="assets/img/completion/completion-auto-y.svg"
 | 
			
		||||
                        [alt]="'addon.mod_h5pactivity.attempt_completion_yes' | translate">
 | 
			
		||||
                    <img *ngIf="!attempt.completion" src="assets/img/completion/completion-auto-n.svg"
 | 
			
		||||
                        [alt]="'addon.mod_h5pactivity.attempt_completion_no' | translate">
 | 
			
		||||
                </ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center addon-mod_h5pactivity-table-success-col">
 | 
			
		||||
                    <ion-icon *ngIf="attempt.success !== null && attempt.success" name="fa-check-circle"
 | 
			
		||||
                        [attr.aria-label]="'addon.mod_h5pactivity.attempt_success_pass' | translate">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
                    <ion-icon *ngIf="attempt.success !== null && !attempt.success" name="far-circle"
 | 
			
		||||
                        [attr.aria-label]="'addon.mod_h5pactivity.attempt_success_fail' | translate">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
                    <img *ngIf="attempt.success === null" src="assets/img/icons/empty.svg"
 | 
			
		||||
                        [alt]="'addon.mod_h5pactivity.attempt_success_unknown' | translate">
 | 
			
		||||
                </ion-col>
 | 
			
		||||
            </ion-row>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
// (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 { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { AddonModH5PActivityUserAttemptsPage } from './user-attempts';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonModH5PActivityUserAttemptsPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModH5PActivityUserAttemptsPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityUserAttemptsPageModule {}
 | 
			
		||||
@ -0,0 +1,10 @@
 | 
			
		||||
:host {
 | 
			
		||||
    .addon-mod_h5pactivity-table-header {
 | 
			
		||||
        --detail-icon-opacity: 0;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-table-row .addon-mod_h5pactivity-table-success-col {
 | 
			
		||||
        font-size: 1.2em;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										146
									
								
								src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,146 @@
 | 
			
		||||
// (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 { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModH5PActivity,
 | 
			
		||||
    AddonModH5PActivityAttempt,
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityUserAttempts,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays user attempts of a certain user.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-h5pactivity-user-attempts',
 | 
			
		||||
    templateUrl: 'user-attempts.html',
 | 
			
		||||
    styleUrls: ['user-attempts.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityUserAttemptsPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    cmId!: number;
 | 
			
		||||
    h5pActivity?: AddonModH5PActivityData;
 | 
			
		||||
    attemptsData?: AddonModH5PActivityUserAttempts;
 | 
			
		||||
    user?: CoreUserProfile;
 | 
			
		||||
    isCurrentUser = false;
 | 
			
		||||
 | 
			
		||||
    protected userId!: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
 | 
			
		||||
        this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId();
 | 
			
		||||
        this.isCurrentUser = this.userId == CoreSites.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     */
 | 
			
		||||
    doRefresh(refresher: IonRefresher): void {
 | 
			
		||||
        this.refreshData().finally(() => {
 | 
			
		||||
            refresher.complete();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get quiz data and attempt data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            this.fetchAttempts(),
 | 
			
		||||
            this.fetchUserProfile(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get attempts.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchAttempts(): Promise<void> {
 | 
			
		||||
        this.attemptsData = await AddonModH5PActivity.getUserAttempts(this.h5pActivity!.id, {
 | 
			
		||||
            cmId: this.cmId,
 | 
			
		||||
            userId: this.userId,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get user profile.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchUserProfile(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            this.user = await CoreUser.getProfile(this.userId, this.courseId, true);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async refreshData(): Promise<void> {
 | 
			
		||||
        const promises = [
 | 
			
		||||
            AddonModH5PActivity.invalidateActivityData(this.courseId),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (this.h5pActivity) {
 | 
			
		||||
            promises.push(AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, this.userId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.ignoreErrors(Promise.all(promises));
 | 
			
		||||
 | 
			
		||||
        await this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open the page to view an attempt.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     */
 | 
			
		||||
    openAttempt(attempt: AddonModH5PActivityAttempt): void {
 | 
			
		||||
        CoreNavigator.navigate(`../../attemptresults/${attempt.id}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/assets/img/icons/empty.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/assets/img/icons/empty.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 299 B  | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user