diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index 1f507b5b8..85b2f0917 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -20,6 +20,7 @@ import { AddonFilterModule } from './filter/filter.module'; import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module'; import { AddonBadgesModule } from './badges/badges.module'; import { AddonCalendarModule } from './calendar/calendar.module'; +import { AddonCourseCompletionModule } from './coursecompletion/coursecompletion.module'; import { AddonNotificationsModule } from './notifications/notifications.module'; import { AddonMessageOutputModule } from './messageoutput/messageoutput.module'; import { AddonMessagesModule } from './messages/messages.module'; @@ -35,6 +36,7 @@ import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module'; AddonBadgesModule, AddonBlogModule, AddonCalendarModule, + AddonCourseCompletionModule, AddonMessagesModule, AddonPrivateFilesModule, AddonFilterModule, diff --git a/src/addons/block/completionstatus/services/block-handler.ts b/src/addons/block/completionstatus/services/block-handler.ts index b62f94926..602be93b2 100644 --- a/src/addons/block/completionstatus/services/block-handler.ts +++ b/src/addons/block/completionstatus/services/block-handler.ts @@ -29,21 +29,14 @@ export class AddonBlockCompletionStatusHandlerService extends CoreBlockBaseHandl blockName = 'completionstatus'; /** - * Returns the data needed to render the block. - * - * @param block The block to render. - * @param contextLevel The context where the block will be used. - * @param instanceId The instance ID associated with the context level. - * @return Data or promise resolved with the data. + * @inheritdoc */ getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { - // @todo - return { title: 'addon.block_completionstatus.pluginname', class: 'addon-block-completion-status', component: CoreBlockOnlyTitleComponent, - link: 'AddonCourseCompletionReportPage', + link: 'coursecompletion', linkParams: { courseId: instanceId, }, diff --git a/src/addons/block/selfcompletion/services/block-handler.ts b/src/addons/block/selfcompletion/services/block-handler.ts index e8a8e6783..01bed3b35 100644 --- a/src/addons/block/selfcompletion/services/block-handler.ts +++ b/src/addons/block/selfcompletion/services/block-handler.ts @@ -29,22 +29,17 @@ export class AddonBlockSelfCompletionHandlerService extends CoreBlockBaseHandler blockName = 'selfcompletion'; /** - * Returns the data needed to render the block. - * - * @param block The block to render. - * @param contextLevel The context where the block will be used. - * @param instanceId The instance ID associated with the context level. - * @return Data or promise resolved with the data. + * @inheritdoc */ getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { - // @todo - return { title: 'addon.block_selfcompletion.pluginname', class: 'addon-block-self-completion', component: CoreBlockOnlyTitleComponent, - link: 'AddonCourseCompletionReportPage', - linkParams: { courseId: instanceId }, + link: 'coursecompletion', + linkParams: { + courseId: instanceId, + }, }; } diff --git a/src/addons/coursecompletion/coursecompletion-lazy.module.ts b/src/addons/coursecompletion/coursecompletion-lazy.module.ts new file mode 100644 index 000000000..2231a59b8 --- /dev/null +++ b/src/addons/coursecompletion/coursecompletion-lazy.module.ts @@ -0,0 +1,41 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { CoreCommentsComponentsModule } from '@features/comments/components/components.module'; +import { CoreTagComponentsModule } from '@features/tag/components/components.module'; +import { AddonCourseCompletionReportPage } from './pages/report/report'; + +const routes: Routes = [ + { + path: '', + component: AddonCourseCompletionReportPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + CoreCommentsComponentsModule, + CoreTagComponentsModule, + ], + exports: [RouterModule], + declarations: [ + AddonCourseCompletionReportPage, + ], +}) +export class AddonCourseCompletionLazyModule {} diff --git a/src/addons/coursecompletion/coursecompletion.module.ts b/src/addons/coursecompletion/coursecompletion.module.ts index ccc8af630..024980c8b 100644 --- a/src/addons/coursecompletion/coursecompletion.module.ts +++ b/src/addons/coursecompletion/coursecompletion.module.ts @@ -12,33 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; -// @todo import { AddonCourseCompletionCourseOptionHandler } from './services/course-option-handler'; -// @todo import { AddonCourseCompletionUserHandler } from './services/user-handler'; -// @todo import { AddonCourseCompletionComponentsModule } from './components/components.module'; -// @todo import { CoreCourseOptionsDelegate } from '@features/course/services/options-delegate'; -// @todo import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { AddonCourseCompletionProvider } from './services/coursecompletion'; +import { AddonCourseCompletionCourseOptionHandler } from './services/handlers/course-option'; +import { AddonCourseCompletionUserHandler } from './services/handlers/user'; + +export const ADDON_COURSECOMPLETION_SERVICES: Type[] = [ + AddonCourseCompletionProvider, +]; + +const routes: Routes = [ + { + path: 'coursecompletion', + loadChildren: () => import('./coursecompletion-lazy.module').then(m => m.AddonCourseCompletionLazyModule), + }, +]; @NgModule({ imports: [ - // AddonCourseCompletionComponentsModule, + CoreMainMenuTabRoutingModule.forChild(routes), + CoreCourseIndexRoutingModule.forChild({ children: routes }), ], providers: [ - // AddonCourseCompletionCourseOptionHandler, - // AddonCourseCompletionUserHandler, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => async () => { + CoreUserDelegate.registerHandler(AddonCourseCompletionUserHandler.instance); + CoreCourseOptionsDelegate.registerHandler(AddonCourseCompletionCourseOptionHandler.instance); + }, + }, ], }) -export class AddonCourseCompletionModule { - - /* @todo constructor( - courseOptionsDelegate: CoreCourseOptionsDelegate, - courseOptionHandler: AddonCourseCompletionCourseOptionHandler, - userDelegate: CoreUserDelegate, - userHandler: AddonCourseCompletionUserHandler, - ) { - // Register handlers. - courseOptionsDelegate.registerHandler(courseOptionHandler); - userDelegate.registerHandler(userHandler); - }*/ - -} +export class AddonCourseCompletionModule {} diff --git a/src/addons/coursecompletion/pages/report/report.html b/src/addons/coursecompletion/pages/report/report.html new file mode 100644 index 000000000..cff8b4432 --- /dev/null +++ b/src/addons/coursecompletion/pages/report/report.html @@ -0,0 +1,92 @@ + + + + + + {{ 'addon.coursecompletion.coursecompletion' | translate }} + + + + + + + + + + +

{{ 'addon.coursecompletion.status' | translate }}

+

{{ statusText! | translate }}

+
+
+ + +

{{ 'addon.coursecompletion.required' | translate }}

+

{{ 'addon.coursecompletion.criteriarequiredall' | translate }}

+

{{ 'addon.coursecompletion.criteriarequiredany' | translate }}

+
+
+
+ + + {{ 'addon.coursecompletion.requiredcriteria' | translate }} + + + +

+

+
+ {{ criteria.status }} +
+ + + + {{ 'addon.coursecompletion.criteriagroup' | translate }} + {{ 'addon.coursecompletion.criteria' | translate }} + {{ 'addon.coursecompletion.requirement' | translate }} + {{ 'addon.coursecompletion.status' | translate }} + {{ 'addon.coursecompletion.complete' | translate }} + {{ 'addon.coursecompletion.completiondate' | translate }} + + + + + + + + + + + + + + + {{ criteria.status }} + + {{ criteria.timecompleted * 1000 | coreFormatDate :'strftimedatetimeshort' }} + + + + + +
+ + + {{ 'addon.coursecompletion.manualselfcompletion' | translate }} + + + + + {{ 'addon.coursecompletion.completecourse' | translate }} + + + + + + + + + {{ 'addon.coursecompletion.nottracked' | translate }} + + +
+
diff --git a/src/addons/coursecompletion/pages/report/report.ts b/src/addons/coursecompletion/pages/report/report.ts new file mode 100644 index 000000000..5cda867eb --- /dev/null +++ b/src/addons/coursecompletion/pages/report/report.ts @@ -0,0 +1,112 @@ +// (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 { + AddonCourseCompletion, + AddonCourseCompletionCourseCompletionStatus, +} from '@addons/coursecompletion/services/coursecompletion'; +import { Component, OnInit } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; + +/** + * Page that displays the course completion report. + */ +@Component({ + selector: 'page-addon-course-completion-report', + templateUrl: 'report.html', +}) +export class AddonCourseCompletionReportPage implements OnInit { + + protected courseId!: number; + protected userId!: number; + + completionLoaded = false; + completion?: AddonCourseCompletionCourseCompletionStatus; + showSelfComplete = false; + tracked = true; // Whether completion is tracked. + statusText?: string; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId(); + + if (!this.userId) { + this.userId = CoreSites.getCurrentSiteUserId(); + } + + this.fetchCompletion().finally(() => { + this.completionLoaded = true; + }); + } + + /** + * Fetch compleiton data. + * + * @return Promise resolved when done. + */ + protected async fetchCompletion(): Promise { + try { + this.completion = await AddonCourseCompletion.getCompletion(this.courseId, this.userId); + + this.statusText = AddonCourseCompletion.getCompletedStatusText(this.completion); + this.showSelfComplete = AddonCourseCompletion.canMarkSelfCompleted(this.userId, this.completion); + + this.tracked = true; + } catch (error) { + if (error && error.errorcode == 'notenroled') { + // Not enrolled error, probably a teacher. + this.tracked = false; + } else { + CoreDomUtils.showErrorModalDefault(error, 'addon.coursecompletion.couldnotloadreport', true); + } + } + } + + /** + * Refresh completion data on PTR. + * + * @param refresher Refresher instance. + */ + async refreshCompletion(refresher?: IonRefresher): Promise { + await AddonCourseCompletion.invalidateCourseCompletion(this.courseId, this.userId).finally(() => { + this.fetchCompletion().finally(() => { + refresher?.complete(); + }); + }); + } + + /** + * Mark course as completed. + */ + async completeCourse(): Promise { + const modal = await CoreDomUtils.showModalLoading('core.sending', true); + + try { + await AddonCourseCompletion.markCourseAsSelfCompleted(this.courseId); + + await this.refreshCompletion(); + } catch (error) { + CoreDomUtils.showErrorModal(error); + } finally { + modal.dismiss(); + } + } + +} diff --git a/src/addons/coursecompletion/services/handlers/course-option.ts b/src/addons/coursecompletion/services/handlers/course-option.ts new file mode 100644 index 000000000..7fbef1c50 --- /dev/null +++ b/src/addons/coursecompletion/services/handlers/course-option.ts @@ -0,0 +1,96 @@ +// (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 { CoreCourseProvider } from '@features/course/services/course'; +import { + CoreCourseAccess, + CoreCourseOptionsHandler, + CoreCourseOptionsHandlerData, +} from '@features/course/services/course-options-delegate'; +import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; +import { makeSingleton } from '@singletons'; +import { AddonCourseCompletion } from '../coursecompletion'; + +/** + * Handler to inject an option into the course main menu. + */ +@Injectable({ providedIn: 'root' }) +export class AddonCourseCompletionCourseOptionHandlerService implements CoreCourseOptionsHandler { + + name = 'AddonCourseCompletion'; + priority = 200; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return AddonCourseCompletion.isPluginViewEnabled(); + } + + /** + * @inheritdoc + */ + async isEnabledForCourse(courseId: number, accessData: CoreCourseAccess): Promise { + if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { + return false; // Not enabled for guests. + } + + const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(courseId); + // If is not enabled in the course, is not enabled for the user. + if (!courseEnabled) { + return false; + } + + return AddonCourseCompletion.isPluginViewEnabledForUser(courseId); + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreCourseOptionsHandlerData | Promise { + return { + title: 'addon.coursecompletion.completionmenuitem', + class: 'addon-coursecompletion-course-handler', + page: 'coursecompletion', + }; + } + + /** + * @inheritdoc + */ + async invalidateEnabledForCourse(courseId: number): Promise { + await AddonCourseCompletion.invalidateCourseCompletion(courseId); + } + + /** + * @inheritdoc + */ + async prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise { + try { + await AddonCourseCompletion.getCompletion(course.id, undefined, { + getFromCache: false, + emergencyCache: false, + }); + } catch (error) { + if (error && error.errorcode == 'notenroled') { + // Not enrolled error, probably a teacher. Ignore error. + } else { + throw error; + } + } + } + +} +export const AddonCourseCompletionCourseOptionHandler = makeSingleton(AddonCourseCompletionCourseOptionHandlerService); diff --git a/src/addons/coursecompletion/services/handlers/user.ts b/src/addons/coursecompletion/services/handlers/user.ts new file mode 100644 index 000000000..1f913caf1 --- /dev/null +++ b/src/addons/coursecompletion/services/handlers/user.ts @@ -0,0 +1,98 @@ +// (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 { CoreUserProfile, CoreUserProvider } from '@features/user/services/user'; +import { CoreUserProfileHandler, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; +import { CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { AddonCourseCompletion } from '../coursecompletion'; + +/** + * Profile course completion handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonCourseCompletionUserHandlerService implements CoreUserProfileHandler { + + name = 'AddonCourseCompletion'; + type = CoreUserDelegateService.TYPE_NEW_PAGE; + priority = 200; + + protected enabledCache = {}; + + constructor() { + CoreEvents.on(CoreEvents.LOGOUT, () => { + this.enabledCache = {}; + }); + + CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { + const cacheKey = data.userId + '-' + data.courseId; + + delete this.enabledCache[cacheKey]; + }); + } + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return AddonCourseCompletion.isPluginViewEnabled(); + } + + /** + * @inheritdoc + */ + async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise { + if (!courseId) { + return false; + } + + const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(courseId); + // If is not enabled in the course, is not enabled for the user. + if (!courseEnabled) { + return false; + } + + const cacheKey = user.id + '-' + courseId; + if (typeof this.enabledCache[cacheKey] !== 'undefined') { + return this.enabledCache[cacheKey]; + } + + const enabled = await AddonCourseCompletion.isPluginViewEnabledForUser(courseId, user.id); + this.enabledCache[cacheKey] = enabled; + + return enabled; + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreUserProfileHandlerData { + return { + icon: 'fas-tasks', + title: 'addon.coursecompletion.coursecompletion', + class: 'addon-coursecompletion-handler', + action: (event, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + CoreNavigator.navigateToSitePath('/coursecompletion', { + params: { courseId, userId: user.id }, + }); + }, + }; + } + +} +export const AddonCourseCompletionUserHandler = makeSingleton(AddonCourseCompletionUserHandlerService); diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index e9a5dab79..95e1d523f 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -117,6 +117,7 @@ import { CoreSitePluginsAssignSubmissionComponent } from '@features/siteplugins/ // Import addon providers. Do not import database module because it causes circular dependencies. import { ADDON_BADGES_SERVICES } from '@addons/badges/badges.module'; import { ADDON_CALENDAR_SERVICES } from '@addons/calendar/calendar.module'; +import { ADDON_COURSECOMPLETION_SERVICES } from '@addons/coursecompletion/coursecompletion.module'; // @todo import { ADDON_COMPETENCY_SERVICES } from '@addons/competency/competency.module'; import { ADDON_MESSAGEOUTPUT_SERVICES } from '@addons/messageoutput/messageoutput.module'; import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module'; @@ -281,6 +282,7 @@ export class CoreCompileProvider { ...extraProviders, ...ADDON_BADGES_SERVICES, ...ADDON_CALENDAR_SERVICES, + ...ADDON_COURSECOMPLETION_SERVICES, // @todo ...ADDON_COMPETENCY_SERVICES, ...ADDON_MESSAGEOUTPUT_SERVICES, ...ADDON_MESSAGES_SERVICES, diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 41a4cd95b..1ba500218 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -820,9 +820,22 @@ export class CoreUserProvider { } } - export const CoreUser = makeSingleton(CoreUserProvider); +declare module '@singletons/events' { + + /** + * Augment CoreEventsData interface with events specific to this service. + * + * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + */ + export interface CoreEventsData { + [CoreUserProvider.PROFILE_REFRESHED]: CoreUserProfileRefreshedData; + [CoreUserProvider.PROFILE_PICTURE_UPDATED]: CoreUserProfilePictureUpdatedData; + } + +} + /** * Data passed to PROFILE_REFRESHED event. */ diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index bf6558671..34ec0ce08 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -309,7 +309,8 @@ img[alt] { // Activity modules .core-module-icon { --size: 24px; - width: auto; + width: var(--size); + height: var(--size); max-width: var(--size); max-height: var(--size); }