From 007835b6b800818725d5a8063dc3a9ce8eb15d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 1 Dec 2021 12:26:46 +0100 Subject: [PATCH] MOBILE-3099 module: Add preview page to show restricted activities --- .../mod/url/services/handlers/module.ts | 9 +- .../course/classes/module-base-handler.ts | 29 ++++- .../module-info/core-course-module-info.html | 13 +- .../components/module-info/module-info.ts | 2 + .../core-course-module-manual-completion.html | 8 +- .../module-navigation/module-navigation.ts | 41 +++--- .../components/module/core-course-module.html | 4 +- .../core-course-unsupported-module.html | 3 - .../features/course/course-lazy.module.ts | 6 +- .../pages/module-preview/module-preview.html | 65 ++++++++++ .../module-preview.module.ts} | 8 +- .../module-preview/module-preview.page.ts | 118 ++++++++++++++++++ .../unsupported-module.html | 27 ---- .../unsupported-module.page.ts | 54 -------- .../services/handlers/default-module.ts | 18 ++- .../course/services/module-delegate.ts | 31 +++++ .../classes/handlers/module-handler.ts | 35 ++++-- 17 files changed, 323 insertions(+), 148 deletions(-) create mode 100644 src/core/features/course/pages/module-preview/module-preview.html rename src/core/features/course/pages/{unsupported-module/unsupported-module.module.ts => module-preview/module-preview.module.ts} (83%) create mode 100644 src/core/features/course/pages/module-preview/module-preview.page.ts delete mode 100644 src/core/features/course/pages/unsupported-module/unsupported-module.html delete mode 100644 src/core/features/course/pages/unsupported-module/unsupported-module.page.ts diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts index e6257f845..69c7d2c4b 100644 --- a/src/addons/mod/url/services/handlers/module.ts +++ b/src/addons/mod/url/services/handlers/module.ts @@ -19,7 +19,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand import { CoreCourse } from '@features/course/services/course'; import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; -import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { CoreNavigationOptions } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -90,12 +90,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple if (shouldOpen) { openUrl(module, courseId); } else { - options = options || {}; - options.params = options.params || {}; - Object.assign(options.params, { module }); - const routeParams = '/' + courseId + '/' + module.id; - - CoreNavigator.navigateToSitePath(AddonModUrlModuleHandlerService.PAGE_NAME + routeParams, options); + this.openActivityPage(module, courseId, options); } } finally { modal.dismiss(); diff --git a/src/core/features/course/classes/module-base-handler.ts b/src/core/features/course/classes/module-base-handler.ts index 474fb231c..3d59a9d00 100644 --- a/src/core/features/course/classes/module-base-handler.ts +++ b/src/core/features/course/classes/module-base-handler.ts @@ -51,14 +51,31 @@ export class CoreModuleHandlerBase implements Partial { courseId: number, options?: CoreNavigationOptions, ): Promise => { - options = options || {}; - options.params = options.params || {}; - Object.assign(options.params, { module }); - const routeParams = '/' + courseId + '/' + module.id; - - await CoreNavigator.navigateToSitePath(this.pageName + routeParams, options); + await this.openActivityPage(module, courseId, options); }, }; } + /** + * Opens the activity page. + * + * @param module The module object. + * @param courseId The course ID. + * @param options Options for the navigation. + * @return Promise resolved when done. + */ + async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise { + if (!CoreCourse.moduleHasView(module)) { + return; + } + + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + + const routeParams = '/' + courseId + '/' + module.id; + + await CoreNavigator.navigateToSitePath(this.pageName + routeParams, options); + } + } diff --git a/src/core/features/course/components/module-info/core-course-module-info.html b/src/core/features/course/components/module-info/core-course-module-info.html index 7afb15368..702b6ff54 100644 --- a/src/core/features/course/components/module-info/core-course-module-info.html +++ b/src/core/features/course/components/module-info/core-course-module-info.html @@ -13,24 +13,25 @@ + [contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="expandDescription ? null : 120"> - + -
+

{{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}

- + diff --git a/src/core/features/course/components/module-info/module-info.ts b/src/core/features/course/components/module-info/module-info.ts index 063ebc88f..2c958c99f 100644 --- a/src/core/features/course/components/module-info/module-info.ts +++ b/src/core/features/course/components/module-info/module-info.ts @@ -36,12 +36,14 @@ import { CoreSites } from '@services/sites'; export class CoreCourseModuleInfoComponent implements OnInit { @Input() module!: CoreCourseModule; // The module to render. + @Input() showManualCompletion = true; // Whether to show manual completion, true by default. @Input() courseId!: number; // The courseId the module belongs to. @Input() component!: string; // Component for format text directive. @Input() componentId!: string | number; // Component ID to use in conjunction with the component. @Input() description?: string | false; // The description to display. If false, no description will be shown. + @Input() expandDescription = false; // If the description should be expanded by default. @Input() hasDataToSync = false; // If the activity has any data to be synced. diff --git a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html index ab4055ff0..d205a5a65 100644 --- a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html +++ b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html @@ -2,20 +2,22 @@ - + {{ 'core.course.completion_manual:done' | translate }} - + {{ 'core.course.completion_manual:markdone' | translate }} - + {{ 'core.course.completion_manual:markdone' | translate }} diff --git a/src/core/features/course/components/module-navigation/module-navigation.ts b/src/core/features/course/components/module-navigation/module-navigation.ts index 627f0bbd5..dfb9602a4 100644 --- a/src/core/features/course/components/module-navigation/module-navigation.ts +++ b/src/core/features/course/components/module-navigation/module-navigation.ts @@ -14,10 +14,11 @@ import { Component, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/course/services/course'; -import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModule, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { IonContent } from '@ionic/angular'; import { ScrollDetail } from '@ionic/core'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; @@ -41,6 +42,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { nextModule?: CoreCourseModule; previousModule?: CoreCourseModule; + nextModuleSection?: CoreCourseSection; + previousModuleSection?: CoreCourseSection; loaded = false; protected element: HTMLElement; @@ -201,9 +204,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { for (let j = startModule; j < section.modules.length && this.nextModule == undefined; j++) { const module = section.modules[j]; - const found = await this.isModuleAvailable(module, section.id); + const found = await this.isModuleAvailable(module); if (found) { this.nextModule = module; + this.nextModuleSection = section; } } } @@ -224,9 +228,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { for (let j = startModule; j >= 0 && this.previousModule == undefined; j--) { const module = section.modules[j]; - const found = await this.isModuleAvailable(module, section.id); + const found = await this.isModuleAvailable(module); if (found) { this.previousModule = module; + this.previousModuleSection = section; } } } @@ -237,20 +242,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { * Module is visible by the user and it has a specific view (e.g. not a label). * * @param module Module to check. - * @param sectionId Section ID the module belongs to. * @return Wether the module is available to the user or not. */ - protected async isModuleAvailable(module: CoreCourseModule, sectionId: number): Promise { - if (module.uservisible === false || !CoreCourse.instance.moduleHasView(module)) { - return false; - } - - if (!module.handlerData) { - module.handlerData = - await CoreCourseModuleDelegate.getModuleDataFor(module.modname, module, this.courseId, sectionId); - } - - return !!module.handlerData?.action; + protected async isModuleAvailable(module: CoreCourseModule): Promise { + return CoreCourse.instance.moduleHasView(module); } /** @@ -291,11 +286,19 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { return; } - if (!module.handlerData?.action) { - return; + if (module.uservisible === false) { + const section = next ? this.nextModuleSection : this.previousModuleSection; + const options: CoreNavigationOptions = { + replace: true, + params: { + module, + section, + }, + }; + CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options); + } else { + CoreCourseModuleDelegate.openActivityPage(module.modname, module, this.courseId, { replace: true }); } - - module.handlerData.action(new Event('click'), module, this.courseId, { replace: true }); } /** diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index ea1bec5df..65637562c 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -75,8 +75,8 @@
- diff --git a/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html b/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html index 6b7fe1848..a55256ca3 100644 --- a/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html +++ b/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html @@ -1,6 +1,3 @@ - - -

{{ 'core.whoops' | translate }}

{{ 'core.uhoh' | translate }}

diff --git a/src/core/features/course/course-lazy.module.ts b/src/core/features/course/course-lazy.module.ts index 80f28334e..5233864dd 100644 --- a/src/core/features/course/course-lazy.module.ts +++ b/src/core/features/course/course-lazy.module.ts @@ -23,9 +23,9 @@ const routes: Routes = [ loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule), }, { - path: ':courseId/unsupported-module', - loadChildren: () => import('./pages/unsupported-module/unsupported-module.module') - .then( m => m.CoreCourseUnsupportedModulePageModule), + path: ':courseId/:cmId/module-preview', + loadChildren: () => import('./pages/module-preview/module-preview.module') + .then( m => m.CoreCourseModulePreviewPageModule), }, { path: ':courseId/list-mod-type', diff --git a/src/core/features/course/pages/module-preview/module-preview.html b/src/core/features/course/pages/module-preview/module-preview.html new file mode 100644 index 000000000..8f352f3b5 --- /dev/null +++ b/src/core/features/course/pages/module-preview/module-preview.html @@ -0,0 +1,65 @@ + + + + + + +

+ + +

+
+ + + + + + + +
+
+ + + + + + + +
+ + + +
+
+ + {{ 'core.course.hiddenfromstudents' | translate }} + +
+
+ + {{ 'core.course.hiddenoncoursepage' | translate }} + +
+
+ {{ 'core.restricted' | translate }} +
+ + +
+
+
+ + {{ 'core.course.manualcompletionnotsynced' | translate }} + +
+ + +
+
+ + +
diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.module.ts b/src/core/features/course/pages/module-preview/module-preview.module.ts similarity index 83% rename from src/core/features/course/pages/unsupported-module/unsupported-module.module.ts rename to src/core/features/course/pages/module-preview/module-preview.module.ts index 596d96650..ab346fcb0 100644 --- a/src/core/features/course/pages/unsupported-module/unsupported-module.module.ts +++ b/src/core/features/course/pages/module-preview/module-preview.module.ts @@ -16,13 +16,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; -import { CoreCourseUnsupportedModulePage } from './unsupported-module.page'; +import { CoreCourseModulePreviewPage } from './module-preview.page'; import { CoreCourseComponentsModule } from '@features/course/components/components.module'; const routes: Routes = [ { path: '', - component: CoreCourseUnsupportedModulePage, + component: CoreCourseModulePreviewPage, }, ]; @@ -33,8 +33,8 @@ const routes: Routes = [ CoreCourseComponentsModule, ], declarations: [ - CoreCourseUnsupportedModulePage, + CoreCourseModulePreviewPage, ], exports: [RouterModule], }) -export class CoreCourseUnsupportedModulePageModule {} +export class CoreCourseModulePreviewPageModule { } diff --git a/src/core/features/course/pages/module-preview/module-preview.page.ts b/src/core/features/course/pages/module-preview/module-preview.page.ts new file mode 100644 index 000000000..51dc29b23 --- /dev/null +++ b/src/core/features/course/pages/module-preview/module-preview.page.ts @@ -0,0 +1,118 @@ +// (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 { CoreCourse } from '@features/course/services/course'; +import { CoreCourseHelper, CoreCourseModule, CoreCourseSection } from '@features/course/services/course-helper'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; + +/** + * Page that displays a module preview. + */ +@Component({ + selector: 'page-core-course-module-preview', + templateUrl: 'module-preview.html', +}) +export class CoreCourseModulePreviewPage implements OnInit { + + title!: string; + module!: CoreCourseModule; + section?: CoreCourseSection; // The section the module belongs to. + courseId!: number; + loaded = false; + unsupported = false; + showManualCompletion = false; + + protected debouncedUpdateModule?: () => void; // Update the module after a certain time. + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + try { + this.module = CoreNavigator.getRequiredRouteParam('module'); + this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + this.section = CoreNavigator.getRouteParam('section'); + } catch (error) { + CoreDomUtils.showErrorModal(error); + + CoreNavigator.back(); + + return; + } + + this.debouncedUpdateModule = CoreUtils.debounce(() => { + this.doRefresh(); + }, 10000); + + await this.fetchModule(); + } + + /** + * Fetch module. + * + * @return Promise resolved when done. + */ + protected async fetchModule(refresh = false): Promise { + if (refresh) { + this.module = await CoreCourse.getModule(this.module.id, this.courseId); + } + + CoreCourseHelper.calculateModuleCompletionData(this.module, this.courseId); + + await CoreCourseHelper.loadModuleOfflineCompletion(this.courseId, this.module); + + this.unsupported = !CoreCourseModuleDelegate.getHandlerName(this.module.modname); + if (!this.unsupported) { + this.module.handlerData = + await CoreCourseModuleDelegate.getModuleDataFor(this.module.modname, this.module, this.courseId); + } + + this.title = this.module.name; + + this.showManualCompletion = await CoreCourseModuleDelegate.manualCompletionAlwaysShown(this.module); + + this.loaded = true; + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: IonRefresher): Promise { + + await CoreCourse.invalidateModule(this.module.id); + + this.fetchModule(true); + + refresher?.complete(); + } + + /** + * The completion of the modules has changed. + * + * @return Promise resolved when done. + */ + async onCompletionChange(): Promise { + // Update the module data after a while. + this.debouncedUpdateModule?.(); + } + +} diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.html b/src/core/features/course/pages/unsupported-module/unsupported-module.html deleted file mode 100644 index 6760fda91..000000000 --- a/src/core/features/course/pages/unsupported-module/unsupported-module.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - -

- - -

-
- - - - - - - - - -
-
- - - diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts b/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts deleted file mode 100644 index 78bb28a44..000000000 --- a/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts +++ /dev/null @@ -1,54 +0,0 @@ -// (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 { CoreCourseWSModule } from '@features/course/services/course'; -import { CoreNavigator } from '@services/navigator'; -import { CoreTextUtils } from '@services/utils/text'; -import { Translate } from '@singletons'; - -/** - * Page that displays info about an unsupported module. - */ -@Component({ - selector: 'page-core-course-unsupported-module', - templateUrl: 'unsupported-module.html', -}) -export class CoreCourseUnsupportedModulePage implements OnInit { - - module?: CoreCourseWSModule; - courseId?: number; - - /** - * @inheritDoc - */ - ngOnInit(): void { - this.module = CoreNavigator.getRouteParam('module'); - this.courseId = CoreNavigator.getRouteNumberParam('courseId'); - } - - /** - * Expand the description. - */ - expandDescription(): void { - CoreTextUtils.viewText(Translate.instant('core.description'), this.module!.description!, { - filter: true, - contextLevel: 'module', - instanceId: this.module!.id, - courseId: this.courseId, - }); - } - -} diff --git a/src/core/features/course/services/handlers/default-module.ts b/src/core/features/course/services/handlers/default-module.ts index 25bf41663..6e8cb25d6 100644 --- a/src/core/features/course/services/handlers/default-module.ts +++ b/src/core/features/course/services/handlers/default-module.ts @@ -49,14 +49,11 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler { icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon), title: module.name, class: 'core-course-default-handler core-course-module-' + module.modname + '-handler', - action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { + action: async (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { event.preventDefault(); event.stopPropagation(); - options = options || {}; - options.params = { module }; - - CoreNavigator.navigateToSitePath('course/' + courseId + '/unsupported-module', options); + await this.openActivityPage(module, courseId, options); }, }; @@ -92,4 +89,15 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler { return true; } + /** + * @inheritdoc + */ + async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + + await CoreNavigator.navigateToSitePath('course/' + courseId + '/' + module.id +'/module-preview', options); + } + } diff --git a/src/core/features/course/services/module-delegate.ts b/src/core/features/course/services/module-delegate.ts index 66dec4184..2776043ff 100644 --- a/src/core/features/course/services/module-delegate.ts +++ b/src/core/features/course/services/module-delegate.ts @@ -102,6 +102,16 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler { * @return Promise resolved with boolean: whether the manual completion should always be displayed. */ manualCompletionAlwaysShown?(module: CoreCourseModule): Promise; + + /** + * Opens the activity page. + * + * @param module The module object. + * @param courseId The course ID. + * @param options Options for the navigation. + * @return Promise resolved when done. + */ + openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise; } /** @@ -295,6 +305,27 @@ export class CoreCourseModuleDelegateService extends CoreDelegate { + return await this.executeFunctionOnEnabled( + modname, + 'openActivityPage', + [module, courseId, options], + ); + } + /** * Check if a certain module type is disabled in a site. * diff --git a/src/core/features/siteplugins/classes/handlers/module-handler.ts b/src/core/features/siteplugins/classes/handlers/module-handler.ts index 1276a3a87..847d2894b 100644 --- a/src/core/features/siteplugins/classes/handlers/module-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/module-handler.ts @@ -15,7 +15,7 @@ import { Type } from '@angular/core'; import { CoreConstants } from '@/core/constants'; -import { CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; @@ -92,17 +92,16 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp if (this.handlerSchema.method) { // There is a method, add an action. - handlerData.action = (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { + handlerData.action = async ( + event: Event, + module: CoreCourseModule, + courseId: number, + options?: CoreNavigationOptions, + ) => { event.preventDefault(); event.stopPropagation(); - options = options || {}; - options.params = { - title: module.name, - module, - }; - - CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options); + await this.openActivityPage(module, courseId, options); }; } @@ -229,4 +228,22 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp return false; } + /** + * @inheritdoc + */ + async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise { + if (!CoreCourse.moduleHasView(module)) { + return; + } + + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { + title: module.name, + module, + }); + + CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options); + } + }