diff --git a/scripts/langindex.json b/scripts/langindex.json index d53076099..bd312bc51 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -659,12 +659,39 @@ "addon.mod_glossary.noentriesfound": "local_moodlemobileapp", "addon.mod_glossary.searchquery": "local_moodlemobileapp", "addon.mod_glossary.tagarea_glossary_entries": "glossary", + "addon.mod_h5pactivity.all_attempts": "h5pactivity", + "addon.mod_h5pactivity.answer_checked": "h5pactivity", + "addon.mod_h5pactivity.answer_correct": "h5pactivity", + "addon.mod_h5pactivity.answer_fail": "h5pactivity", + "addon.mod_h5pactivity.answer_incorrect": "h5pactivity", + "addon.mod_h5pactivity.answer_pass": "h5pactivity", + "addon.mod_h5pactivity.attempt": "h5pactivity", + "addon.mod_h5pactivity.attempt_completion_no": "h5pactivity", + "addon.mod_h5pactivity.attempt_completion_yes": "h5pactivity", + "addon.mod_h5pactivity.attempt_success_fail": "h5pactivity", + "addon.mod_h5pactivity.attempt_success_pass": "h5pactivity", + "addon.mod_h5pactivity.attempt_success_unknown": "h5pactivity", + "addon.mod_h5pactivity.attempts_none": "h5pactivity", + "addon.mod_h5pactivity.completion": "h5pactivity", "addon.mod_h5pactivity.downloadh5pfile": "local_moodlemobileapp", + "addon.mod_h5pactivity.duration": "h5pactivity", "addon.mod_h5pactivity.errorgetactivity": "local_moodlemobileapp", "addon.mod_h5pactivity.filestatenotdownloaded": "local_moodlemobileapp", "addon.mod_h5pactivity.filestateoutdated": "local_moodlemobileapp", + "addon.mod_h5pactivity.maxscore": "h5pactivity", "addon.mod_h5pactivity.modulenameplural": "h5pactivity", + "addon.mod_h5pactivity.myattempts": "h5pactivity", + "addon.mod_h5pactivity.no_compatible_track": "h5pactivity", "addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp", + "addon.mod_h5pactivity.outcome": "h5pactivity", + "addon.mod_h5pactivity.result_fill-in": "h5pactivity", + "addon.mod_h5pactivity.result_other": "h5pactivity", + "addon.mod_h5pactivity.review_my_attempts": "h5pactivity", + "addon.mod_h5pactivity.score": "h5pactivity", + "addon.mod_h5pactivity.score_out_of": "h5pactivity", + "addon.mod_h5pactivity.startdate": "h5pactivity", + "addon.mod_h5pactivity.totalscore": "h5pactivity", + "addon.mod_h5pactivity.viewattempt": "local_moodlemobileapp", "addon.mod_imscp.deploymenterror": "imscp", "addon.mod_imscp.modulenameplural": "imscp", "addon.mod_imscp.showmoduledescription": "local_moodlemobileapp", diff --git a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html index 53638796a..f2acd6230 100644 --- a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html +++ b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html @@ -1,6 +1,7 @@ + diff --git a/src/addon/mod/h5pactivity/components/index/index.ts b/src/addon/mod/h5pactivity/components/index/index.ts index 8f452818b..bf879ebe0 100644 --- a/src/addon/mod/h5pactivity/components/index/index.ts +++ b/src/addon/mod/h5pactivity/components/index/index.ts @@ -319,6 +319,13 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv AddonModH5PActivity.instance.logView(this.h5pActivity.id, this.h5pActivity.name, this.siteId); } + /** + * Go to view user events. + */ + viewMyAttempts(): void { + this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id}); + } + /** * Component destroyed. */ diff --git a/src/addon/mod/h5pactivity/h5pactivity.module.ts b/src/addon/mod/h5pactivity/h5pactivity.module.ts index 771ac2ad6..3053c1168 100644 --- a/src/addon/mod/h5pactivity/h5pactivity.module.ts +++ b/src/addon/mod/h5pactivity/h5pactivity.module.ts @@ -23,6 +23,7 @@ import { AddonModH5PActivityModuleHandler } from './providers/module-handler'; import { AddonModH5PActivityProvider } from './providers/h5pactivity'; import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler'; import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler'; +import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler'; // List of providers (without handlers). export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [ @@ -40,6 +41,7 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [ AddonModH5PActivityModuleHandler, AddonModH5PActivityPrefetchHandler, AddonModH5PActivityIndexLinkHandler, + AddonModH5PActivityReportLinkHandler, ] }) export class AddonModH5PActivityModule { @@ -48,10 +50,12 @@ export class AddonModH5PActivityModule { prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModH5PActivityPrefetchHandler, linksDelegate: CoreContentLinksDelegate, - indexHandler: AddonModH5PActivityIndexLinkHandler) { + indexHandler: AddonModH5PActivityIndexLinkHandler, + reportLinkHandler: AddonModH5PActivityReportLinkHandler) { moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); linksDelegate.registerHandler(indexHandler); + linksDelegate.registerHandler(reportLinkHandler); } } diff --git a/src/addon/mod/h5pactivity/lang/en.json b/src/addon/mod/h5pactivity/lang/en.json index 2e7efde5b..21bb9eecd 100644 --- a/src/addon/mod/h5pactivity/lang/en.json +++ b/src/addon/mod/h5pactivity/lang/en.json @@ -1,8 +1,35 @@ { + "all_attempts": "All user attempts", + "answer_checked": "Answer checked", + "answer_correct": "Your answer is correct", + "answer_fail": "Incorrect answer", + "answer_incorrect": "Your answer is incorrect", + "answer_pass": "Correct answer", + "attempt": "Attempt", + "attempt_completion_no": "This attempt is not marked as completed", + "attempt_completion_yes": "This attempt is completed", + "attempts_none": "This user has no attempts to display.", + "attempt_success_fail": "Fail", + "attempt_success_pass": "Pass", + "attempt_success_unknown": "Not reported", + "completion": "Completion", "downloadh5pfile": "Download H5P file", + "duration": "Duration", "errorgetactivity": "Error getting H5P activity data.", "filestatenotdownloaded": "The H5P package is not downloaded. You need to download it to be able to use it.", "filestateoutdated": "The H5P package has been modified since the last download. You need to download it again to be able to use it.", + "maxscore": "Max score", "modulenameplural": "H5P", - "offlinedisabledwarning": "You will need to be online to view the H5P package." + "myattempts": "My attempts", + "no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.", + "offlinedisabledwarning": "You will need to be online to view the H5P package.", + "outcome": "Outcome", + "result_fill-in": "Fill-in text", + "result_other": "Unkown interaction type", + "review_my_attempts": "View my attempts", + "score": "Score", + "score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}", + "startdate": "Start date", + "totalscore": "Total score", + "viewattempt": "View attempt {{$a}}" } \ No newline at end of file diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.html b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.html new file mode 100644 index 000000000..4ff03aa34 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + +

{{ '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/addon/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts new file mode 100644 index 000000000..a692f5b14 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts @@ -0,0 +1,35 @@ +// (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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonModH5PActivityAttemptResultsPage } from './attempt-results'; + +@NgModule({ + declarations: [ + AddonModH5PActivityAttemptResultsPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonModH5PActivityAttemptResultsPage), + TranslateModule.forChild(), + ], +}) +export class AddonModH5PActivityAttemptResultsPageModule {} diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.scss b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.scss new file mode 100644 index 000000000..04b50c7d4 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.scss @@ -0,0 +1,55 @@ +ion-app.app-root page-addon-mod-h5pactivity-attempt-results { + + .addon-mod_h5pactivity-attempt-result-summary { + img { + width: 16px; + height: 16px; + display: inline; + @include margin-horizontal(0, 4px); + } + + .icon { + font-size: 1.4em; + } + } + + .addon-mod_h5pactivity-result-table-header .item-inner { + font-size: 0.9em; + font-weight: bold; + + .col[text-center] { + @include padding-horizontal(0); + } + } + + .addon-mod_h5pactivity-result-table-header, .addon-mod_h5pactivity-result-table-row { + + .item-inner ion-label { + @include margin(null, 0, null, null); + } + + .item { + @include padding(null, null, null, 0); + } + + .label { + margin-top: 0; + margin-bottom: 0; + } + + .icon { + font-size: 1.2em; + } + } + + .addon-mod_h5pactivity-result-table-row.item:nth-child(even) { + background-color: $gray-lighter; + @include darkmode() { + background-color: $core-dark-item-divider-bg-color; + } + } + + .addon-mod_h5pactivity-result-score { + border-top: 1px solid black; + } +} diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts new file mode 100644 index 000000000..96b99fc16 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts @@ -0,0 +1,137 @@ +// (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 { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreUser } from '@core/user/providers/user'; +import { + AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAttemptResults +} from '../../providers/h5pactivity'; + +/** + * Page that displays results of an attempt. + */ +@IonicPage({ segment: 'addon-mod-h5pactivity-attempt-results' }) +@Component({ + selector: 'page-addon-mod-h5pactivity-attempt-results', + templateUrl: 'attempt-results.html', +}) +export class AddonModH5PActivityAttemptResultsPage implements OnInit { + loaded: boolean; + h5pActivity: AddonModH5PActivityData; + attempt: AddonModH5PActivityAttemptResults; + user: any; + component = AddonModH5PActivityProvider.COMPONENT; + + protected courseId: number; + protected h5pActivityId: number; + protected attemptId: number; + + constructor(navParams: NavParams) { + this.courseId = navParams.get('courseId'); + this.h5pActivityId = navParams.get('h5pActivityId'); + this.attemptId = navParams.get('attemptId'); + } + + /** + * Component being initialized. + * + * @return Promise resolved when done. + */ + async ngOnInit(): Promise { + try { + await this.fetchData(); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading attempt.'); + } finally { + this.loaded = true; + } + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + doRefresh(refresher: any): void { + this.refreshData().finally(() => { + refresher.complete(); + }); + } + + /** + * Get quiz data and attempt data. + * + * @return Promise resolved when done. + */ + protected async fetchData(): Promise { + await Promise.all([ + this.fetchActivity(), + this.fetchAttempt(), + ]); + + await this.fetchUserProfile(); + } + + /** + * Get activity data. + * + * @return Promise resolved when done. + */ + protected async fetchActivity(): Promise { + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); + } + + /** + * Get attempts. + * + * @return Promise resolved when done. + */ + protected async fetchAttempt(): Promise { + this.attempt = await AddonModH5PActivity.instance.getAttemptResults(this.h5pActivityId, this.attemptId); + } + + /** + * Get user profile. + * + * @return Promise resolved when done. + */ + protected async fetchUserProfile(): Promise { + try { + this.user = await CoreUser.instance.getProfile(this.attempt.userid, this.courseId, true); + } catch (error) { + // Ignore errors. + } + } + + /** + * Refresh the data. + * + * @return Promise resolved when done. + */ + protected async refreshData(): Promise { + + try { + await Promise.all([ + AddonModH5PActivity.instance.invalidateActivityData(this.courseId), + AddonModH5PActivity.instance.invalidateAttemptResults(this.h5pActivityId, this.attemptId), + ]); + } catch (error) { + // Ignore errors. + } + + await this.fetchData(); + } +} diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.html b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.html new file mode 100644 index 000000000..85822234a --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + + +

{{ 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 }} + + + + + + + + + + + + \ No newline at end of file diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts new file mode 100644 index 000000000..8532f1bb4 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts @@ -0,0 +1,35 @@ +// (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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonModH5PActivityUserAttemptsPage } from './user-attempts'; + +@NgModule({ + declarations: [ + AddonModH5PActivityUserAttemptsPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonModH5PActivityUserAttemptsPage), + TranslateModule.forChild(), + ], +}) +export class AddonModH5PActivityUserAttemptsPageModule {} diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.scss b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.scss new file mode 100644 index 000000000..f6a9381b0 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.scss @@ -0,0 +1,37 @@ +ion-app.app-root page-addon-mod-h5pactivity-user-attempts { + + .item.addon-mod_h5pactivity-table-header[detail-push] .item-inner { + background-image: none; + } + + .item.addon-mod_h5pactivity-table-header .item-inner { + font-size: 0.9em; + font-weight: bold; + + .col[text-center] { + @include padding-horizontal(0); + } + } + + .addon-mod_h5pactivity-table-header, .addon-mod_h5pactivity-table-row { + + .item-inner ion-label { + @include margin(null, 0, null, null); + } + + .item { + @include padding(null, null, null, 0); + } + + .label { + margin-top: 0; + margin-bottom: 0; + } + } + + .addon-mod_h5pactivity-table-row { + .addon-mod_h5pactivity-table-success-col { + font-size: 1.4em; + } + } +} diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts new file mode 100644 index 000000000..cc8499405 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts @@ -0,0 +1,138 @@ +// (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 { IonicPage, NavParams } from 'ionic-angular'; +import { CoreSites } from '@providers/sites'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreUser } from '@core/user/providers/user'; +import { + AddonModH5PActivity, AddonModH5PActivityData, AddonModH5PActivityUserAttempts +} from '../../providers/h5pactivity'; + +/** + * Page that displays user attempts of a certain user. + */ +@IonicPage({ segment: 'addon-mod-h5pactivity-user-attempts' }) +@Component({ + selector: 'page-addon-mod-h5pactivity-user-attempts', + templateUrl: 'user-attempts.html', +}) +export class AddonModH5PActivityUserAttemptsPage implements OnInit { + loaded: boolean; + courseId: number; + h5pActivityId: number; + h5pActivity: AddonModH5PActivityData; + attemptsData: AddonModH5PActivityUserAttempts; + user: any; + isCurrentUser: boolean; + + protected userId: number; + + constructor(navParams: NavParams) { + this.courseId = navParams.get('courseId'); + this.h5pActivityId = navParams.get('h5pActivityId'); + this.userId = navParams.get('userId') || CoreSites.instance.getCurrentSiteUserId(); + this.isCurrentUser = this.userId == CoreSites.instance.getCurrentSiteUserId(); + } + + /** + * Component being initialized. + * + * @return Promise resolved when done. + */ + async ngOnInit(): Promise { + try { + await this.fetchData(); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading attempts.'); + } finally { + this.loaded = true; + } + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + doRefresh(refresher: any): void { + this.refreshData().finally(() => { + refresher.complete(); + }); + } + + /** + * Get quiz data and attempt data. + * + * @return Promise resolved when done. + */ + protected async fetchData(): Promise { + await Promise.all([ + this.fetchActivity(), + this.fetchAttempts(), + this.fetchUserProfile(), + ]); + } + + /** + * Get activity data. + * + * @return Promise resolved when done. + */ + protected async fetchActivity(): Promise { + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); + } + + /** + * Get attempts. + * + * @return Promise resolved when done. + */ + protected async fetchAttempts(): Promise { + this.attemptsData = await AddonModH5PActivity.instance.getUserAttempts(this.h5pActivityId, { userId: this.userId }); + } + + /** + * Get user profile. + * + * @return Promise resolved when done. + */ + protected async fetchUserProfile(): Promise { + try { + this.user = await CoreUser.instance.getProfile(this.userId, this.courseId, true); + } catch (error) { + // Ignore errors. + } + } + + /** + * Refresh the data. + * + * @return Promise resolved when done. + */ + protected async refreshData(): Promise { + + try { + await Promise.all([ + AddonModH5PActivity.instance.invalidateActivityData(this.courseId), + AddonModH5PActivity.instance.invalidateUserAttempts(this.h5pActivityId, this.userId), + ]); + } catch (error) { + // Ignore errors. + } + + await this.fetchData(); + } +} diff --git a/src/addon/mod/h5pactivity/providers/h5pactivity.ts b/src/addon/mod/h5pactivity/providers/h5pactivity.ts index 70d42524b..dd9ff494d 100644 --- a/src/addon/mod/h5pactivity/providers/h5pactivity.ts +++ b/src/addon/mod/h5pactivity/providers/h5pactivity.ts @@ -16,6 +16,8 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@providers/sites'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreTimeUtils } from '@providers/utils/time'; +import { CoreUtils } from '@providers/utils/utils'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseLogHelper } from '@core/course/providers/log-helper'; import { CoreH5P } from '@core/h5p/providers/h5p'; @@ -32,6 +34,78 @@ export class AddonModH5PActivityProvider { protected ROOT_CACHE_KEY = 'mmaModH5PActivity:'; + /** + * Format an attempt's data. + * + * @param attempt Attempt to format. + */ + protected formatAttempt(attempt: AddonModH5PActivityWSAttempt): AddonModH5PActivityAttempt { + const formattedAttempt: AddonModH5PActivityAttempt = attempt; + + formattedAttempt.timecreated = attempt.timecreated * 1000; // Convert to milliseconds. + formattedAttempt.timemodified = attempt.timemodified * 1000; // Convert to milliseconds. + formattedAttempt.success = typeof formattedAttempt.success == 'undefined' ? null : formattedAttempt.success; + + if (!attempt.duration) { + formattedAttempt.durationReadable = '-'; + formattedAttempt.durationCompact = '-'; + } else { + formattedAttempt.durationReadable = CoreTimeUtils.instance.formatTime(attempt.duration); + formattedAttempt.durationCompact = CoreTimeUtils.instance.formatDurationShort(attempt.duration); + } + + return formattedAttempt; + } + + /** + * Format attempt data and results. + * + * @param attempt Attempt and results to format. + */ + protected formatAttemptResults(attempt: AddonModH5PActivityWSAttemptResults): AddonModH5PActivityAttemptResults { + const formattedAttempt: AddonModH5PActivityAttemptResults = this.formatAttempt(attempt); + + formattedAttempt.results = formattedAttempt.results.map((result) => { + return this.formatResult(result); + }); + + return formattedAttempt; + } + + /** + * Format the attempts of a user. + * + * @param data Data to format. + * @return Formatted data. + */ + protected formatUserAttempts(data: AddonModH5PActivityWSUserAttempts): AddonModH5PActivityUserAttempts { + const formatted: AddonModH5PActivityUserAttempts = data; + + formatted.attempts = formatted.attempts.map((attempt) => { + return this.formatAttempt(attempt); + }); + + if (formatted.scored) { + + formatted.scored.attempts = formatted.scored.attempts.map((attempt) => { + return this.formatAttempt(attempt); + }); + } + + return formatted; + } + + /** + * Format an attempt's result. + * + * @param result Result to format. + */ + protected formatResult(result: AddonModH5PActivityWSResult): AddonModH5PActivityWSResult { + result.timecreated = result.timecreated * 1000; // Convert to milliseconds. + + return result; + } + /** * Get cache key for access information WS calls. * @@ -65,6 +139,157 @@ export class AddonModH5PActivityProvider { return site.read('mod_h5pactivity_get_h5pactivity_access_information', params, preSets); } + /** + * Get attempt results for all user attempts. + * + * @param id Activity ID. + * @param options Other options. + * @return Promise resolved with the results of the attempt. + */ + async getAllAttemptsResults(id: number, options?: AddonModH5PActivityGetAttemptResultsOptions) + : Promise { + + const userAttempts = await AddonModH5PActivity.instance.getUserAttempts(id, options); + + const attemptIds = userAttempts.attempts.map((attempt) => { + return attempt.id; + }); + + if (attemptIds.length) { + // Get all the attempts with a single call. + return AddonModH5PActivity.instance.getAttemptsResults(id, attemptIds, options); + } else { + // No attempts. + return { + activityid: id, + attempts: [], + warnings: [], + }; + } + } + + /** + * Get cache key for results WS calls. + * + * @param id Instance ID. + * @param attemptsIds Attempts IDs. + * @return Cache key. + */ + protected getAttemptResultsCacheKey(id: number, attemptsIds: number[]): string { + return this.getAttemptResultsCommonCacheKey(id) + ':' + JSON.stringify(attemptsIds); + } + + /** + * Get common cache key for results WS calls. + * + * @param id Instance ID. + * @return Cache key. + */ + protected getAttemptResultsCommonCacheKey(id: number): string { + return this.ROOT_CACHE_KEY + 'results:' + id; + } + + /** + * Get attempt results. + * + * @param id Activity ID. + * @param attemptId Attempt ID. + * @param options Other options. + * @return Promise resolved with the results of the attempt. + */ + async getAttemptResults(id: number, attemptId: number, options?: AddonModH5PActivityGetAttemptResultsOptions) + : Promise { + + options = options || {}; + + const site = await CoreSites.instance.getSite(options.siteId); + + const params = { + h5pactivityid: id, + attemptids: [attemptId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (options.forceCache) { + preSets.omitExpires = true; + } else if (options.ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + try { + const response: AddonModH5PActivityGetResultsResult = await site.read('mod_h5pactivity_get_results', params, preSets); + + if (response.warnings[0]) { + throw response.warnings[0]; // Cannot view attempt. + } + + return this.formatAttemptResults(response.attempts[0]); + } catch (error) { + if (CoreUtils.instance.isWebServiceError(error)) { + throw error; + } + + // Check if the full list of results is cached. If so, get the results from there. + options.forceCache = true; + + const attemptsResults = await AddonModH5PActivity.instance.getAllAttemptsResults(id, options); + + const attempt = attemptsResults.attempts.find((attempt) => { + return attempt.id == attemptId; + }); + + if (!attempt) { + throw error; + } + + return attempt; + } + } + + /** + * Get attempts results. + * + * @param id Activity ID. + * @param attemptsIds Attempts IDs. + * @param options Other options. + * @return Promise resolved with all the attempts. + */ + async getAttemptsResults(id: number, attemptsIds: number[], options?: AddonModH5PActivityGetAttemptResultsOptions) + : Promise { + + options = options || {}; + + const site = await CoreSites.instance.getSite(options.siteId); + + const params = { + h5pactivityid: id, + attemptids: attemptsIds, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getAttemptResultsCommonCacheKey(id), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (options.forceCache) { + preSets.omitExpires = true; + } else if (options.ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + const response: AddonModH5PActivityGetResultsResult = await site.read('mod_h5pactivity_get_results', params, preSets); + + response.attempts = response.attempts.map((attempt) => { + return this.formatAttemptResults(attempt); + }); + + return response; + } + /** * Get deployed file from an H5P activity instance. * @@ -172,6 +397,65 @@ export class AddonModH5PActivityProvider { return this.getH5PActivityByField(courseId, 'id', id, forceCache, siteId); } + /** + * Get cache key for attemps WS calls. + * + * @param id Instance ID. + * @param userIds User IDs. + * @return Cache key. + */ + protected getUserAttemptsCacheKey(id: number, userIds: number[]): string { + return this.getUserAttemptsCommonCacheKey(id) + ':' + JSON.stringify(userIds); + } + + /** + * Get common cache key for attempts WS calls. + * + * @param id Instance ID. + * @return Cache key. + */ + protected getUserAttemptsCommonCacheKey(id: number): string { + return this.ROOT_CACHE_KEY + 'attempts:' + id; + } + + /** + * Get attempts of a certain user. + * + * @param id Activity ID. + * @param options Other options. + * @return Promise resolved with the attempts of the user. + */ + async getUserAttempts(id: number, options?: AddonModH5PActivityGetAttemptsOptions): Promise { + + options = options || {}; + + const site = await CoreSites.instance.getSite(options.siteId); + + const params = { + h5pactivityid: id, + userids: [options.userId || site.getUserId()], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserAttemptsCacheKey(id, params.userids), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (options.forceCache) { + preSets.omitExpires = true; + } else if (options.ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + const response: AddonModH5PActivityGetAttemptsResult = await site.read('mod_h5pactivity_get_attempts', params, preSets); + + if (response.warnings[0]) { + throw response.warnings[0]; // Cannot view user attempts. + } + + return this.formatUserAttempts(response.usersattempts[0]); + } + /** * Invalidates access information. * @@ -199,6 +483,62 @@ export class AddonModH5PActivityProvider { await site.invalidateWsCacheForKey(this.getH5PActivityDataCacheKey(courseId)); } + /** + * Invalidates all attempts results for H5P activity. + * + * @param id Activity ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllResults(id: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getAttemptResultsCommonCacheKey(id)); + } + + /** + * Invalidates results of a certain attempt for H5P activity. + * + * @param id Activity ID. + * @param attemptId Attempt ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAttemptResults(id: number, attemptId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getAttemptResultsCacheKey(id, [attemptId])); + } + + /** + * Invalidates all users attempts for H5P activity. + * + * @param id Activity ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllUserAttempts(id: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getUserAttemptsCommonCacheKey(id)); + } + + /** + * Invalidates attempts of a certain user for H5P activity. + * + * @param id Activity ID. + * @param userId User ID. If not defined, current user in the site. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateUserAttempts(id: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + userId = userId || site.getUserId(); + + await site.invalidateWsCacheForKey(this.getUserAttemptsCacheKey(id, [userId])); + } + /** * Delete launcher. * @@ -286,6 +626,141 @@ export type AddonModH5PActivityAccessInfo = { canreviewattempts?: boolean; // Whether the user has the capability mod/h5pactivity:reviewattempts allowed. }; +/** + * Result of WS mod_h5pactivity_get_attempts. + */ +export type AddonModH5PActivityGetAttemptsResult = { + activityid: number; // Activity course module ID. + usersattempts: AddonModH5PActivityWSUserAttempts[]; // The complete users attempts list. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_h5pactivity_get_results. + */ +export type AddonModH5PActivityGetResultsResult = { + activityid: number; // Activity course module ID. + attempts: AddonModH5PActivityWSAttemptResults[]; // The complete attempts list. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Attempts data for a user as returned by the WS mod_h5pactivity_get_attempts. + */ +export type AddonModH5PActivityWSUserAttempts = { + userid: number; // The user id. + attempts: AddonModH5PActivityWSAttempt[]; // The complete attempts list. + scored?: { // Attempts used to grade the activity. + title: string; // Scored attempts title. + grademethod: string; // Scored attempts title. + attempts: AddonModH5PActivityWSAttempt[]; // List of the grading attempts. + }; +}; + +/** + * Attempt data as returned by the WS mod_h5pactivity_get_attempts. + */ +export type AddonModH5PActivityWSAttempt = { + id: number; // ID of the context. + h5pactivityid: number; // ID of the H5P activity. + userid: number; // ID of the user. + timecreated: number; // Attempt creation. + timemodified: number; // Attempt modified. + attempt: number; // Attempt number. + rawscore: number; // Attempt score value. + maxscore: number; // Attempt max score. + duration: number; // Attempt duration in seconds. + completion?: number; // Attempt completion. + success?: number; // Attempt success. + scaled: number; // Attempt scaled. +}; + +/** + * Attempt and results data as returned by the WS mod_h5pactivity_get_results. + */ +export type AddonModH5PActivityWSAttemptResults = AddonModH5PActivityWSAttempt & { + results?: AddonModH5PActivityWSResult[]; // The results of the attempt. +}; + +/** + * Attempt result data as returned by the WS mod_h5pactivity_get_results. + */ +export type AddonModH5PActivityWSResult = { + id: number; // ID of the context. + attemptid: number; // ID of the H5P attempt. + subcontent: string; // Subcontent identifier. + timecreated: number; // Result creation. + interactiontype: string; // Interaction type. + description: string; // Result description. + content?: string; // Result extra content. + rawscore: number; // Result score value. + maxscore: number; // Result max score. + duration?: number; // Result duration in seconds. + completion?: number; // Result completion. + success?: number; // Result success. + optionslabel?: string; // Label used for result options. + correctlabel?: string; // Label used for correct answers. + answerlabel?: string; // Label used for user answers. + track?: boolean; // If the result has valid track information. + options?: { // The statement options. + description: string; // Option description. + id: number; // Option identifier. + correctanswer: AddonModH5PActivityWSResultAnswer; // The option correct answer. + useranswer: AddonModH5PActivityWSResultAnswer; // The option user answer. + }[]; +}; + +/** + * Result answer as returned by the WS mod_h5pactivity_get_results. + */ +export type AddonModH5PActivityWSResultAnswer = { + answer?: string; // Option text value. + correct?: boolean; // If has to be displayed as correct. + incorrect?: boolean; // If has to be displayed as incorrect. + text?: boolean; // If has to be displayed as simple text. + checked?: boolean; // If has to be displayed as a checked option. + unchecked?: boolean; // If has to be displayed as a unchecked option. + pass?: boolean; // If has to be displayed as passed. + fail?: boolean; // If has to be displayed as failed. +}; + +/** + * User attempts data with some calculated data. + */ +export type AddonModH5PActivityUserAttempts = { + userid: number; // The user id. + attempts: AddonModH5PActivityAttempt[]; // The complete attempts list. + scored?: { // Attempts used to grade the activity. + title: string; // Scored attempts title. + grademethod: string; // Scored attempts title. + attempts: AddonModH5PActivityAttempt[]; // List of the grading attempts. + }; +}; + +/** + * Attempts results with some calculated data. + */ +export type AddonModH5PActivityAttemptsResults = { + activityid: number; // Activity course module ID. + attempts: AddonModH5PActivityAttemptResults[]; // The complete attempts list. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Attempt with some calculated data. + */ +export type AddonModH5PActivityAttempt = AddonModH5PActivityWSAttempt & { + durationReadable?: string; // Duration in a human readable format. + durationCompact?: string; // Duration in a "short" human readable format. +}; + +/** + * Attempt and results data with some calculated data. + */ +export type AddonModH5PActivityAttemptResults = AddonModH5PActivityAttempt & { + results?: AddonModH5PActivityWSResult[]; // The results of the attempt. +}; + /** * Options to pass to getDeployedFile function. */ @@ -294,3 +769,18 @@ export type AddonModH5PActivityGetDeployedFileOptions = { ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down. siteId?: string; // Site ID. If not defined, current site. }; + +/** + * Options to pass to getAttemptResults function. + */ +export type AddonModH5PActivityGetAttemptResultsOptions = { + forceCache?: boolean; // Whether to force cache. If not cached, it will call the WS. + ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down. + siteId?: string; // Site ID. If not defined, current site. + userId?: number; // User ID. If not defined, user of the site. +}; + +/** + * Options to pass to getAttempts function. + */ +export type AddonModH5PActivityGetAttemptsOptions = AddonModH5PActivityGetAttemptResultsOptions; diff --git a/src/addon/mod/h5pactivity/providers/prefetch-handler.ts b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts index ccb03a168..85a804baf 100644 --- a/src/addon/mod/h5pactivity/providers/prefetch-handler.ts +++ b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable, Injector } from '@angular/core'; +import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; @@ -26,7 +26,7 @@ import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/acti import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreH5PHelper } from '@core/h5p/classes/helper'; import { CoreH5P } from '@core/h5p/providers/h5p'; -import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreUser } from '@core/user/providers/user'; import { AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData } from './h5pactivity'; /** @@ -47,9 +47,7 @@ export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefet sitesProvider: CoreSitesProvider, domUtils: CoreDomUtilsProvider, filterHelper: CoreFilterHelperProvider, - pluginFileDelegate: CorePluginFileDelegate, - protected userProvider: CoreUserProvider, - protected injector: Injector) { + pluginFileDelegate: CorePluginFileDelegate) { super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper, pluginFileDelegate); @@ -137,7 +135,7 @@ export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefet const introFiles = this.getIntroFilesFromInstance(module, h5pActivity); await Promise.all([ - AddonModH5PActivity.instance.getAccessInformation(h5pActivity.id, true, siteId), + this.prefetchWSData(h5pActivity, siteId), this.filepoolProvider.addFilesToQueue(siteId, introFiles, AddonModH5PActivityProvider.COMPONENT, module.id), this.prefetchMainFile(module, h5pActivity, siteId), ]); @@ -163,4 +161,31 @@ export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefet await this.filepoolProvider.addFilesToQueue(siteId, [deployedFile], AddonModH5PActivityProvider.COMPONENT, module.id); } + + /** + * Prefetch all the WebService data. + * + * @param h5pActivity Activity instance. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchWSData(h5pActivity: AddonModH5PActivityData, siteId: string): Promise { + + const accessInfo = await AddonModH5PActivity.instance.getAccessInformation(h5pActivity.id, true, siteId); + + if (!accessInfo.canreviewattempts) { + // Not a teacher, prefetch user attempts and the current user profile. + const site = await this.sitesProvider.getSite(siteId); + + const options = { + ignoreCache: true, + siteId: siteId, + }; + + await Promise.all([ + AddonModH5PActivity.instance.getAllAttemptsResults(h5pActivity.id, options), + CoreUser.instance.prefetchProfiles([site.getUserId()], h5pActivity.course, siteId), + ]); + } + } } diff --git a/src/addon/mod/h5pactivity/providers/report-link-handler.ts b/src/addon/mod/h5pactivity/providers/report-link-handler.ts new file mode 100644 index 000000000..615b151a7 --- /dev/null +++ b/src/addon/mod/h5pactivity/providers/report-link-handler.ts @@ -0,0 +1,147 @@ +// (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 { Injectable } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelper } from '@core/contentlinks/providers/helper'; +import { CoreCourse } from '@core/course/providers/course'; +import { AddonModH5PActivity } from './h5pactivity'; + +/** + * Handler to treat links to H5P activity report. + */ +@Injectable() +export class AddonModH5PActivityReportLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModH5PActivityReportLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModH5PActivity'; + pattern = /\/mod\/h5pactivity\/report\.php.*([\&\?]a=\d+)/; + + constructor() { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param siteIds List of sites the URL belongs to. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @return List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + courseId = courseId || params.courseid || params.cid; + + return [{ + action: async (siteId, navCtrl?): Promise => { + try { + const id = Number(params.a); + + if (!courseId) { + courseId = await this.getCourseId(id, siteId); + } + + if (typeof params.attemptid != 'undefined') { + this.openAttemptResults(id, Number(params.attemptid), courseId, siteId, navCtrl); + } else { + const userId = params.userid ? Number(params.userid) : undefined; + + this.openUserAttempts(id, courseId, siteId, userId, navCtrl); + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error processing link.'); + } + } + }]; + } + + /** + * Get course Id for an activity. + * + * @param id Activity ID. + * @param siteId Site ID. + * @return Promise resolved with course ID. + */ + protected async getCourseId(id: number, siteId: string): Promise { + const modal = CoreDomUtils.instance.showModalLoading(); + + try { + const module = await CoreCourse.instance.getModuleBasicInfoByInstance(id, 'h5pactivity', siteId); + + return module.course; + } finally { + modal.dismiss(); + } + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param siteId The site ID. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @return Whether the handler is enabled for the URL and site. + */ + async isEnabled(siteId: string, url: string, params: any, courseId?: number): Promise { + return AddonModH5PActivity.instance.isPluginEnabled(); + } + + /** + * Open attempt results. + * + * @param id Activity ID. + * @param attemptId Attempt ID. + * @param courseId Course ID. + * @param siteId Site ID. + * @param navCtrl The NavController to use to navigate. + * @return Promise resolved when done. + */ + protected openAttemptResults(id: number, attemptId: number, courseId: number, siteId: string, navCtrl?: NavController): void { + + const pageParams = { + courseId: courseId, + h5pActivityId: id, + attemptId: attemptId, + }; + + CoreContentLinksHelper.instance.goInSite(navCtrl, 'AddonModH5PActivityAttemptResultsPage', pageParams, siteId); + } + + /** + * Open user attempts. + * + * @param id Activity ID. + * @param courseId Course ID. + * @param siteId Site ID. + * @param userId User ID. If not defined, current user in site. + * @param navCtrl The NavController to use to navigate. + * @return Promise resolved when done. + */ + protected openUserAttempts(id: number, courseId: number, siteId: string, userId?: number, navCtrl?: NavController): void { + + const pageParams = { + courseId: courseId, + h5pActivityId: id, + userId: userId, + }; + + CoreContentLinksHelper.instance.goInSite(navCtrl, 'AddonModH5PActivityUserAttemptsPage', pageParams, siteId); + } +} 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 diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 919a4c0a0..edb8dfe7a 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -659,12 +659,39 @@ "addon.mod_glossary.noentriesfound": "No entries were found.", "addon.mod_glossary.searchquery": "Search query", "addon.mod_glossary.tagarea_glossary_entries": "Glossary entries", + "addon.mod_h5pactivity.all_attempts": "All user attempts", + "addon.mod_h5pactivity.answer_checked": "Answer checked", + "addon.mod_h5pactivity.answer_correct": "Your answer is correct", + "addon.mod_h5pactivity.answer_fail": "Incorrect answer", + "addon.mod_h5pactivity.answer_incorrect": "Your answer is incorrect", + "addon.mod_h5pactivity.answer_pass": "Correct answer", + "addon.mod_h5pactivity.attempt": "Attempt", + "addon.mod_h5pactivity.attempt_completion_no": "This attempt is not marked as completed", + "addon.mod_h5pactivity.attempt_completion_yes": "This attempt is completed", + "addon.mod_h5pactivity.attempt_success_fail": "Fail", + "addon.mod_h5pactivity.attempt_success_pass": "Pass", + "addon.mod_h5pactivity.attempt_success_unknown": "Not reported", + "addon.mod_h5pactivity.attempts_none": "This user has no attempts to display.", + "addon.mod_h5pactivity.completion": "Completion", "addon.mod_h5pactivity.downloadh5pfile": "Download H5P file", + "addon.mod_h5pactivity.duration": "Duration", "addon.mod_h5pactivity.errorgetactivity": "Error getting H5P activity data.", "addon.mod_h5pactivity.filestatenotdownloaded": "The H5P package is not downloaded. You need to download it to be able to use it.", "addon.mod_h5pactivity.filestateoutdated": "The H5P package has been modified since the last download. You need to download it again to be able to use it.", + "addon.mod_h5pactivity.maxscore": "Max score", "addon.mod_h5pactivity.modulenameplural": "H5P", + "addon.mod_h5pactivity.myattempts": "My attempts", + "addon.mod_h5pactivity.no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.", "addon.mod_h5pactivity.offlinedisabledwarning": "You will need to be online to view the H5P package.", + "addon.mod_h5pactivity.outcome": "Outcome", + "addon.mod_h5pactivity.result_fill-in": "Fill-in text", + "addon.mod_h5pactivity.result_other": "Unkown interaction type", + "addon.mod_h5pactivity.review_my_attempts": "View my attempts", + "addon.mod_h5pactivity.score": "Score", + "addon.mod_h5pactivity.score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}", + "addon.mod_h5pactivity.startdate": "Start date", + "addon.mod_h5pactivity.totalscore": "Total score", + "addon.mod_h5pactivity.viewattempt": "View attempt {{$a}}", "addon.mod_imscp.deploymenterror": "Content package error!", "addon.mod_imscp.modulenameplural": "IMS content packages", "addon.mod_imscp.showmoduledescription": "Show description", diff --git a/src/components/icon/icon.scss b/src/components/icon/icon.scss index a80559c8e..f14ba848a 100644 --- a/src/components/icon/icon.scss +++ b/src/components/icon/icon.scss @@ -7,7 +7,9 @@ @each $color-name, $color-base, $color-contrast in get-colors($colors-dark) { .fa-#{$color-name} { - color: $color-base !important; + @include darkmode() { + color: $color-base !important; + } } } diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index c4969b4f4..dff2fd024 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -75,17 +75,17 @@ export class CoreIframeComponent implements OnInit, OnChanges { const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; this.iframeUtils.treatFrame(iframe, false, navCtrl); + iframe.addEventListener('load', () => { + this.loading = false; + this.loaded.emit(iframe); // Notify iframe was loaded. + }); + + iframe.addEventListener('error', () => { + this.loading = false; + this.domUtils.showErrorModal('core.errorloadingcontent', true); + }); + if (this.loading) { - iframe.addEventListener('load', () => { - this.loading = false; - this.loaded.emit(iframe); // Notify iframe was loaded. - }); - - iframe.addEventListener('error', () => { - this.loading = false; - this.domUtils.showErrorModal('core.errorloadingcontent', true); - }); - setTimeout(() => { this.loading = false; }, this.IFRAME_TIMEOUT); diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index 54bb261c1..fc3d45b5f 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -32,6 +32,8 @@ import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins import { CoreSite } from '@classes/site'; import { CoreMainMenuProvider } from '@core/mainmenu/providers/mainmenu'; +import { makeSingleton } from '@singletons/core.singletons'; + /** * Service that provides some features regarding content links. */ @@ -358,3 +360,5 @@ export class CoreContentLinksHelperProvider { } } } + +export class CoreContentLinksHelper extends makeSingleton(CoreContentLinksHelperProvider) {} diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index 1bbb23c80..4dffe3963 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -22,6 +22,8 @@ import { CoreAppProvider } from '@providers/app'; import { CoreUserOfflineProvider } from './offline'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; +import { makeSingleton } from '@singletons/core.singletons'; + /** * Service to provide user functionalities. */ @@ -733,6 +735,8 @@ export class CoreUserProvider { } } +export class CoreUser extends makeSingleton(CoreUserProvider) {} + /** * Data returned by user_summary_exporter. */ diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index df872eb49..551606ead 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -55,7 +55,7 @@ export class CoreIframeUtilsProvider { checkOnlineFrameInOffline(element: any, isSubframe?: boolean): boolean { const src = element.src || element.data; - if (src && !this.urlUtils.isLocalFileUrl(src) && !this.appProvider.isOnline()) { + if (src && src != 'about:blank' && !this.urlUtils.isLocalFileUrl(src) && !this.appProvider.isOnline()) { if (element.classList.contains('core-iframe-offline-disabled')) { // Iframe already hidden, stop. return true; diff --git a/src/providers/utils/time.ts b/src/providers/utils/time.ts index 470c30f44..158046a5c 100644 --- a/src/providers/utils/time.ts +++ b/src/providers/utils/time.ts @@ -220,7 +220,7 @@ export class CoreTimeUtilsProvider { * Returns hours, minutes and seconds in a human readable format. * * @param duration Duration in seconds - * @param precision Number of elements to have in precission. 0 or undefined to full precission. + * @param precision Number of elements to have in precision. 0 or undefined to full precission. * @return Duration in a human readable format. */ formatDuration(duration: number, precision?: number): string { @@ -253,6 +253,29 @@ export class CoreTimeUtilsProvider { return durationString.trim(); } + /** + * Returns duration in a short human readable format: minutes and seconds, in fromat: 3' 27''. + * + * @param duration Duration in seconds + * @return Duration in a short human readable format. + */ + formatDurationShort(duration: number): string { + + const minutes = Math.floor(duration / 60); + const seconds = duration - minutes * 60; + const durations = []; + + if (minutes > 0) { + durations.push(minutes + '\''); + } + + if (seconds > 0 || minutes === 0) { + durations.push(seconds + '\'\''); + } + + return durations.join(' '); + } + /** * Return the current timestamp in a "readable" format: YYYYMMDDHHmmSS. *