From a3924cbbcbbbde0320caf2751a0db6c6bfe837cb Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 15 Mar 2021 16:32:19 +0100 Subject: [PATCH] MOBILE-3645 h5pactivity: Implement reports pages --- .../h5pactivity/h5pactivity-lazy.module.ts | 10 + .../attempt-results/attempt-results.html | 193 ++++++++++++++++++ .../attempt-results/attempt-results.module.ts | 38 ++++ .../attempt-results/attempt-results.scss | 42 ++++ .../pages/attempt-results/attempt-results.ts | 124 +++++++++++ .../pages/user-attempts/user-attempts.html | 114 +++++++++++ .../user-attempts/user-attempts.module.ts | 38 ++++ .../pages/user-attempts/user-attempts.scss | 10 + .../pages/user-attempts/user-attempts.ts | 146 +++++++++++++ src/assets/img/icons/empty.svg | 3 + 10 files changed, 718 insertions(+) create mode 100644 src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.html create mode 100644 src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts create mode 100644 src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.scss create mode 100644 src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts create mode 100644 src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.html create mode 100644 src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts create mode 100644 src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.scss create mode 100644 src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts create mode 100644 src/assets/img/icons/empty.svg diff --git a/src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts b/src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts index 475e80f52..477958871 100644 --- a/src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts +++ b/src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts @@ -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({ diff --git a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.html b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.html new file mode 100644 index 000000000..308551daf --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.html @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + +

{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}: {{user.fullname}}

+
+
+ + + +

{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}

+
+
+ + + + + + +

{{ 'addon.mod_h5pactivity.startdate' | translate }}

+

{{ attempt.timecreated | coreFormatDate:'strftimedatetime' }}

+
+
+ + +

{{ 'addon.mod_h5pactivity.completion' | translate }}

+

+ + {{ 'addon.mod_h5pactivity.attempt_completion_yes' | translate }} +

+

+ + {{ 'addon.mod_h5pactivity.attempt_completion_no' | translate }} +

+
+
+ + +

{{ 'addon.mod_h5pactivity.duration' | translate }}

+

{{ attempt.durationReadable }}

+
+
+ + +

{{ 'addon.mod_h5pactivity.outcome' | translate }}

+

+ + {{ 'addon.mod_h5pactivity.attempt_success_pass' | translate }} +

+

+ + {{ 'addon.mod_h5pactivity.attempt_success_fail' | translate }} +

+

+ {{ 'addon.mod_h5pactivity.attempt_success_unknown' | translate }} +

+
+
+ + +

{{ 'addon.mod_h5pactivity.totalscore' | translate }}

+

{{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: attempt} }}

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + {{ result.optionslabel }} + {{ result.correctlabel }} + {{ result.answerlabel }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ {{ 'addon.mod_h5pactivity.score' | translate }}: + {{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: result} }} +

+
+
+
+ + + + + + {{ 'addon.mod_h5pactivity.no_compatible_track' | translate:{$a: result.interactiontype} }} + + +
+
+
+
+
+ + + +

+ + + {{ answer.answer }} +

+

+ + + {{ answer.answer }} +

+

+ {{ answer.answer }} +

+

+ + +

+

+ + +

+

+ + +

+
diff --git a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts new file mode 100644 index 000000000..f8560683d --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts @@ -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 {} diff --git a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.scss b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.scss new file mode 100644 index 000000000..e2cfa01c3 --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.scss @@ -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); + } +} diff --git a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts new file mode 100644 index 000000000..741e78f28 --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts @@ -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 { + 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 { + 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 { + 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 { + 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(); + } + +} diff --git a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.html b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.html new file mode 100644 index 000000000..341942790 --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.html @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + +

{{ user.fullname }}

+
+
+ + + +

{{ 'addon.mod_h5pactivity.myattempts' | translate }}

+
+
+ + + + + + +

{{ attemptsData.scored.title }}

+
+
+ + +
+ + + + + +

{{ 'addon.mod_h5pactivity.all_attempts' | translate }}

+
+
+ +
+
+ + + + +
+
+ + + + + + + + # + {{ 'core.date' | translate }} + {{ 'addon.mod_h5pactivity.score' | translate }} + {{ 'addon.mod_h5pactivity.maxscore' | translate }} + {{ 'addon.mod_h5pactivity.duration' | translate }} + {{ 'addon.mod_h5pactivity.completion' | translate }} + {{ 'core.success' | translate }} + + + + + + + + + + {{ attempt.attempt }} + + {{ attempt.timemodified | coreFormatDate:'strftimedatetimeshort' }} + + + {{ attempt.rawscore }} / {{ attempt.maxscore }} + + {{ attempt.maxscore }} + {{ attempt.durationReadable }} + + + + + + + + + + + + + + + diff --git a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts new file mode 100644 index 000000000..810cf13f3 --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts @@ -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 {} diff --git a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.scss b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.scss new file mode 100644 index 000000000..87902ea3e --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.scss @@ -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; + } +} diff --git a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts new file mode 100644 index 000000000..9f6d8b1d1 --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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}`); + } + +} diff --git a/src/assets/img/icons/empty.svg b/src/assets/img/icons/empty.svg new file mode 100644 index 000000000..a16fb7460 --- /dev/null +++ b/src/assets/img/icons/empty.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file