From 7a729d16b8907aacc563023a1dd4bdf65be9e44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 3 Mar 2021 13:19:27 +0100 Subject: [PATCH] MOBILE-3647 label: Add label activity module --- src/addons/mod/book/services/book.ts | 6 +- src/addons/mod/label/label.module.ts | 37 ++++ .../mod/label/services/handlers/index-link.ts | 32 +++ .../mod/label/services/handlers/module.ts | 75 +++++++ .../mod/label/services/handlers/prefetch.ts | 71 +++++++ src/addons/mod/label/services/label.ts | 200 ++++++++++++++++++ src/addons/mod/mod.module.ts | 2 + src/addons/mod/page/services/page.ts | 4 +- src/theme/components/mod-label.scss | 8 + src/theme/theme.scss | 1 + 10 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 src/addons/mod/label/label.module.ts create mode 100644 src/addons/mod/label/services/handlers/index-link.ts create mode 100644 src/addons/mod/label/services/handlers/module.ts create mode 100644 src/addons/mod/label/services/handlers/prefetch.ts create mode 100644 src/addons/mod/label/services/label.ts create mode 100644 src/theme/components/mod-label.scss diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index 7f7fa5bc6..7999730e4 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -25,7 +25,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreTextUtils } from '@services/utils/text'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFile } from '@services/file'; -import { CoreWSError } from '@classes/errors/wserror'; +import { CoreError } from '@classes/errors/error'; /** * Constants to define how the chapters and subchapters of a book should be displayed in that table of contents. @@ -104,7 +104,7 @@ export class AddonModBookProvider { return book; } - throw new CoreWSError('Book not found'); + throw new CoreError('Book not found'); } /** @@ -130,7 +130,7 @@ export class AddonModBookProvider { const indexUrl = contentsMap[chapterId] ? contentsMap[chapterId].indexUrl : undefined; if (!indexUrl) { // It shouldn't happen. - throw new CoreWSError('Could not locate the index chapter.'); + throw new CoreError('Could not locate the index chapter.'); } if (!CoreFile.isAvailable()) { diff --git a/src/addons/mod/label/label.module.ts b/src/addons/mod/label/label.module.ts new file mode 100644 index 000000000..6c480d851 --- /dev/null +++ b/src/addons/mod/label/label.module.ts @@ -0,0 +1,37 @@ +// (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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { AddonModLabelIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModLabelModuleHandler } from './services/handlers/module'; +import { AddonModLabelPrefetchHandler } from './services/handlers/prefetch'; + +@NgModule({ + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModLabelModuleHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModLabelIndexLinkHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModLabelPrefetchHandler.instance); + }, + }, + ], +}) +export class AddonModLabelModule {} diff --git a/src/addons/mod/label/services/handlers/index-link.ts b/src/addons/mod/label/services/handlers/index-link.ts new file mode 100644 index 000000000..e1c176329 --- /dev/null +++ b/src/addons/mod/label/services/handlers/index-link.ts @@ -0,0 +1,32 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to label. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLabelIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModLabelLinkHandler'; + + constructor() { + super('AddonModLabel', 'label', 'l'); + } + +} +export const AddonModLabelIndexLinkHandler = makeSingleton(AddonModLabelIndexLinkHandlerService); diff --git a/src/addons/mod/label/services/handlers/module.ts b/src/addons/mod/label/services/handlers/module.ts new file mode 100644 index 000000000..1bd7161e6 --- /dev/null +++ b/src/addons/mod/label/services/handlers/module.ts @@ -0,0 +1,75 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Injectable } from '@angular/core'; +import { CoreCourseWSModule } from '@features/course/services/course'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to support label modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLabelModuleHandlerService implements CoreCourseModuleHandler { + + name = 'AddonModLabel'; + modName = 'label'; + + supportedFeatures = { + [CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE, + [CoreConstants.FEATURE_IDNUMBER]: true, + [CoreConstants.FEATURE_GROUPS]: false, + [CoreConstants.FEATURE_GROUPINGS]: false, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: false, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getData(module: CoreCourseWSModule): CoreCourseModuleHandlerData { + // Remove the description from the module so it isn't rendered twice. + const title = module.description || ''; + module.description = ''; + + return { + icon: '', + title: title, + a11yTitle: '', + class: 'addon-mod-label-handler', + }; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise { + // There's no need to implement this because label cannot be used in singleactivity course format. + return; + } + +} +export const AddonModLabelModuleHandler = makeSingleton(AddonModLabelModuleHandlerService); diff --git a/src/addons/mod/label/services/handlers/prefetch.ts b/src/addons/mod/label/services/handlers/prefetch.ts new file mode 100644 index 000000000..91c583f43 --- /dev/null +++ b/src/addons/mod/label/services/handlers/prefetch.ts @@ -0,0 +1,71 @@ +// (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 { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreSitesReadingStrategy } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonModLabel, AddonModLabelLabel, AddonModLabelProvider } from '../label'; + +/** + * Handler to prefetch labels. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLabelPrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase { + + name = 'AddonModLabel'; + modName = 'label'; + component = AddonModLabelProvider.COMPONENT; + updatesNames = /^.*files$/; + skipListStatus = true; + + /** + * @inheritdoc + */ + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number, ignoreCache?: boolean): Promise { + let label: AddonModLabelLabel | undefined; + + if (AddonModLabel.isGetLabelAvailableForSite()) { + label = await AddonModLabel.getLabel(courseId, module.id, { + readingStrategy: ignoreCache ? CoreSitesReadingStrategy.OnlyNetwork : undefined, + }); + } + + return this.getIntroFilesFromInstance(module, label); + } + + /** + * @inheritdoc + */ + async invalidateContent(moduleId: number, courseId: number): Promise { + await AddonModLabel.invalidateContent(moduleId, courseId); + } + + /** + * @inheritdoc + */ + async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise { + const promises: Promise[] = []; + + promises.push(AddonModLabel.invalidateLabelData(courseId)); + promises.push(CoreCourse.invalidateModule(module.id)); + + await CoreUtils.allPromises(promises); + } + +} +export const AddonModLabelPrefetchHandler = makeSingleton(AddonModLabelPrefetchHandlerService); diff --git a/src/addons/mod/label/services/label.ts b/src/addons/mod/label/services/label.ts new file mode 100644 index 000000000..387e1ea1e --- /dev/null +++ b/src/addons/mod/label/services/label.ts @@ -0,0 +1,200 @@ +// (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 { CoreError } from '@classes/errors/error'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'mmaModLabel:'; + +/** + * Service that provides some features for labels. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLabelProvider { + + static readonly COMPONENT = 'mmaModLabel'; + + /** + * Get cache key for label data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getLabelDataCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'label:' + courseId; + } + + /** + * Get a label with key=value. If more than one is found, only the first will be returned. + * + * @param courseId Course ID. + * @param key Name of the property to check. + * @param value Value to search. + * @param options Other options. + * @return Promise resolved when the label is retrieved. + */ + protected async getLabelByField( + courseId: number, + key: string, + value: number, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModLabelGetLabelsByCoursesWSParams = { + courseids: [courseId], + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getLabelDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModLabelProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), + }; + + const response = + await site.read('mod_label_get_labels_by_courses', params, preSets); + + const currentLabel = response.labels.find((label) => label[key] == value); + if (currentLabel) { + return currentLabel; + } + + throw new CoreError('Label not found'); + } + + /** + * Get a label by course module ID. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the label is retrieved. + */ + getLabel(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLabelByField(courseId, 'coursemodule', cmId, options); + } + + /** + * Get a label by ID. + * + * @param courseId Course ID. + * @param labelId Label ID. + * @param options Other options. + * @return Promise resolved when the label is retrieved. + */ + getLabelById(courseId: number, labelId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLabelByField(courseId, 'id', labelId, options); + } + + /** + * Invalidate label data. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateLabelData(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getLabelDataCacheKey(courseId)); + } + + /** + * Invalidate the prefetched content. + * + * @param moduleId The module ID. + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when data is invalidated. + */ + async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const promises: Promise[] = []; + + promises.push(this.invalidateLabelData(courseId, siteId)); + promises.push(CoreFilepool.invalidateFilesByComponent(siteId, AddonModLabelProvider.COMPONENT, moduleId, true)); + + await CoreUtils.allPromises(promises); + } + + /** + * Check if the site has the WS to get label data. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether it's available. + * @since 3.3 + */ + async isGetLabelAvailable(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('mod_label_get_labels_by_courses'); + } + + /** + * Check if the site has the WS to get label data. + * + * @param site Site. If not defined, current site. + * @return Whether it's available. + * @since 3.3 + */ + isGetLabelAvailableForSite(site?: CoreSite): boolean { + site = site || CoreSites.getCurrentSite(); + + return !!site?.wsAvailable('mod_label_get_labels_by_courses'); + } + +} +export const AddonModLabel = makeSingleton(AddonModLabelProvider); + + +/** + * Label returned by mod_label_get_labels_by_courses. + */ +export type AddonModLabelLabel = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Label name. + intro: string; // Label contents. + introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + timemodified: number; // Last time the label was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Params of mod_label_get_labels_by_courses WS. + */ +type AddonModLabelGetLabelsByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; + +/** + * Data returned by mod_label_get_labels_by_courses WS. + */ +type AddonModLabelGetLabelsByCoursesWSResponse = { + labels: AddonModLabelLabel[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 687482f10..e0d3f1ab6 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -17,6 +17,7 @@ import { NgModule } from '@angular/core'; import { AddonModAssignModule } from './assign/assign.module'; import { AddonModBookModule } from './book/book.module'; import { AddonModForumModule } from './forum/forum.module'; +import { AddonModLabelModule } from './label/label.module'; import { AddonModLessonModule } from './lesson/lesson.module'; import { AddonModPageModule } from './page/page.module'; import { AddonModQuizModule } from './quiz/quiz.module'; @@ -30,6 +31,7 @@ import { AddonModQuizModule } from './quiz/quiz.module'; AddonModLessonModule, AddonModPageModule, AddonModQuizModule, + AddonModLabelModule, ], providers: [], exports: [], diff --git a/src/addons/mod/page/services/page.ts b/src/addons/mod/page/services/page.ts index cbebdc9a8..5029d42e7 100644 --- a/src/addons/mod/page/services/page.ts +++ b/src/addons/mod/page/services/page.ts @@ -21,7 +21,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreCourse } from '@features/course/services/course'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; -import { CoreWSError } from '@classes/errors/wserror'; +import { CoreError } from '@classes/errors/error'; const ROOT_CACHE_KEY = 'mmaModPage:'; @@ -79,7 +79,7 @@ export class AddonModPageProvider { return currentPage; } - throw new CoreWSError('Page not found'); + throw new CoreError('Page not found'); } /** diff --git a/src/theme/components/mod-label.scss b/src/theme/components/mod-label.scss new file mode 100644 index 000000000..e68f7324f --- /dev/null +++ b/src/theme/components/mod-label.scss @@ -0,0 +1,8 @@ +.item.core-course-module-handler.addon-mod-label-handler { + align-items: center !important; + cursor: auto !important; + + &:hover { + opacity: 1; + } +} diff --git a/src/theme/theme.scss b/src/theme/theme.scss index 9b82fd2d8..265c3521e 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -21,6 +21,7 @@ /* Components */ @import "./components/format-text.scss"; @import "./components/rubrics.scss"; +@import "./components/mod-label.scss"; /* Core CSS required for Ionic components to work properly */ @import "~@ionic/angular/css/core.css";