diff --git a/scripts/langindex.json b/scripts/langindex.json index 592d78cb5..1c93939d1 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -10,6 +10,7 @@ "addon.badges.issuername": "badges", "addon.badges.nobadges": "badges", "addon.badges.recipientdetails": "badges", + "addon.block_activitymodules.pluginname": "block_activity_modules", "addon.block_myoverview.all": "block_myoverview", "addon.block_myoverview.future": "block_myoverview", "addon.block_myoverview.inprogress": "block_myoverview", @@ -1468,6 +1469,7 @@ "core.refresh": "moodle", "core.required": "moodle", "core.requireduserdatamissing": "local_moodlemobileapp", + "core.resources": "moodle", "core.restore": "moodle", "core.retry": "local_moodlemobileapp", "core.save": "moodle", diff --git a/src/addon/block/activitymodules/activitymodules.module.ts b/src/addon/block/activitymodules/activitymodules.module.ts new file mode 100644 index 000000000..88fbb6e06 --- /dev/null +++ b/src/addon/block/activitymodules/activitymodules.module.ts @@ -0,0 +1,44 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonBlockActivityModulesComponentsModule } from './components/components.module'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockActivityModulesHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + CoreComponentsModule, + CoreDirectivesModule, + AddonBlockActivityModulesComponentsModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockActivityModulesHandler + ] +}) +export class AddonBlockActivityModulesModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockActivityModulesHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addon/block/activitymodules/components/activitymodules/activitymodules.ts new file mode 100644 index 000000000..e85cee1e4 --- /dev/null +++ b/src/addon/block/activitymodules/components/activitymodules/activitymodules.ts @@ -0,0 +1,124 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector, Input } from '@angular/core'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component'; +import { CoreConstants } from '@core/constants'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Component to render an "activity modules" block. + */ +@Component({ + selector: 'addon-block-activitymodules', + templateUrl: 'addon-block-activitymodules.html' +}) +export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent implements OnInit { + @Input() block: any; // The block to render. + @Input() contextLevel: string; // The context where the block will be used. + @Input() instanceId: number; // The instance ID associated with the context level. + + entries: any[] = []; + + protected fetchContentDefaultError = 'Error getting activity modules data.'; + + constructor(injector: Injector, protected utils: CoreUtilsProvider, protected courseProvider: CoreCourseProvider, + protected translate: TranslateService, protected moduleDelegate: CoreCourseModuleDelegate) { + + super(injector, 'AddonBlockActivityModulesComponent'); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + } + + /** + * Perform the invalidate content function. + * + * @return {Promise} Resolved when done. + */ + protected invalidateContent(): Promise { + return this.courseProvider.invalidateSections(this.instanceId); + } + + /** + * Fetch the data to render the block. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(): Promise { + return this.courseProvider.getSections(this.instanceId, false, true).then((sections) => { + + this.entries = []; + + const archetypes = {}; + let modFullNames = {}; + + sections.forEach((section) => { + if (!section.modules) { + return; + } + + section.modules.forEach((mod) => { + if (mod.uservisible === false || !this.courseProvider.moduleHasView(mod) || + typeof modFullNames[mod.modname] != 'undefined') { + // Ignore this module. + return; + } + + // Get the archetype of the module type. + if (typeof archetypes[mod.modname] == 'undefined') { + archetypes[mod.modname] = this.moduleDelegate.supportsFeature(mod.modname, + CoreConstants.FEATURE_MOD_ARCHETYPE, CoreConstants.MOD_ARCHETYPE_OTHER); + } + + // Get the full name of the module type. + if (archetypes[mod.modname] == CoreConstants.MOD_ARCHETYPE_RESOURCE) { + // All resources are gathered in a single "Resources" option. + if (!modFullNames['resources']) { + modFullNames['resources'] = this.translate.instant('core.resources'); + } + } else { + modFullNames[mod.modname] = mod.modplural; + } + }); + }); + + // Sort the modnames alphabetically. + modFullNames = this.utils.sortValues(modFullNames); + + for (const modName in modFullNames) { + let icon; + + if (modName === 'resources') { + icon = this.courseProvider.getModuleIconSrc('page'); + } else { + icon = this.courseProvider.getModuleIconSrc(modName); + } + + this.entries.push({ + icon: icon, + name: modFullNames[modName], + modName: modName + }); + } + }); + } +} diff --git a/src/addon/block/activitymodules/components/activitymodules/addon-block-activitymodules.html b/src/addon/block/activitymodules/components/activitymodules/addon-block-activitymodules.html new file mode 100644 index 000000000..226012314 --- /dev/null +++ b/src/addon/block/activitymodules/components/activitymodules/addon-block-activitymodules.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/addon/block/activitymodules/components/components.module.ts b/src/addon/block/activitymodules/components/components.module.ts new file mode 100644 index 000000000..5cbc4cfd4 --- /dev/null +++ b/src/addon/block/activitymodules/components/components.module.ts @@ -0,0 +1,45 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonBlockActivityModulesComponent } from './activitymodules/activitymodules'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; + +@NgModule({ + declarations: [ + AddonBlockActivityModulesComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule + ], + providers: [ + ], + exports: [ + AddonBlockActivityModulesComponent + ], + entryComponents: [ + AddonBlockActivityModulesComponent + ] +}) +export class AddonBlockActivityModulesComponentsModule {} diff --git a/src/addon/block/activitymodules/lang/en.json b/src/addon/block/activitymodules/lang/en.json new file mode 100644 index 000000000..7f1c7ab21 --- /dev/null +++ b/src/addon/block/activitymodules/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Activities" +} diff --git a/src/addon/block/activitymodules/pages/list-type/list-type.html b/src/addon/block/activitymodules/pages/list-type/list-type.html new file mode 100644 index 000000000..b131fb912 --- /dev/null +++ b/src/addon/block/activitymodules/pages/list-type/list-type.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/addon/block/activitymodules/pages/list-type/list-type.module.ts b/src/addon/block/activitymodules/pages/list-type/list-type.module.ts new file mode 100644 index 000000000..1202f9c37 --- /dev/null +++ b/src/addon/block/activitymodules/pages/list-type/list-type.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { AddonBlockActivityModulesListTypePage } from './list-type'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; + +@NgModule({ + declarations: [ + AddonBlockActivityModulesListTypePage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule, + IonicPageModule.forChild(AddonBlockActivityModulesListTypePage), + TranslateModule.forChild() + ], +}) +export class AddonBlockActivityModulesListTypePageModule {} diff --git a/src/addon/block/activitymodules/pages/list-type/list-type.ts b/src/addon/block/activitymodules/pages/list-type/list-type.ts new file mode 100644 index 000000000..3e97500a0 --- /dev/null +++ b/src/addon/block/activitymodules/pages/list-type/list-type.ts @@ -0,0 +1,120 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreConstants } from '@core/constants'; + +/** + * Page that displays comments. + */ +@IonicPage({ segment: 'addon-block-activity-modules-list-type' }) +@Component({ + selector: 'page-addon-block-activity-modules-list-type', + templateUrl: 'list-type.html', +}) +export class AddonBlockActivityModulesListTypePage { + + modules = []; + title: string; + loaded = false; + + protected courseId: number; + protected modName: string; + protected archetypes = {}; // To speed up the check of modules. + + constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private moduleDelegate: CoreCourseModuleDelegate, + private domUtils: CoreDomUtilsProvider, private courseHelper: CoreCourseHelperProvider) { + + this.title = navParams.get('title'); + this.courseId = navParams.get('courseId'); + this.modName = navParams.get('modName'); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchData().finally(() => { + this.loaded = true; + }); + } + + /** + * Fetches the data. + * + * @return {Promise} Resolved when done. + */ + protected fetchData(): Promise { + // Get all the modules in the course. + return this.courseProvider.getSections(this.courseId, false, true).then((sections) => { + + this.modules = []; + + sections.forEach((section) => { + if (!section.modules) { + return; + } + + section.modules.forEach((mod) => { + if (mod.uservisible === false || !this.courseProvider.moduleHasView(mod)) { + // Ignore this module. + return; + } + + if (this.modName === 'resources') { + // Check that the module is a resource. + if (typeof this.archetypes[mod.modname] == 'undefined') { + this.archetypes[mod.modname] = this.moduleDelegate.supportsFeature(mod.modname, + CoreConstants.FEATURE_MOD_ARCHETYPE, CoreConstants.MOD_ARCHETYPE_OTHER); + } + + if (this.archetypes[mod.modname] == CoreConstants.MOD_ARCHETYPE_RESOURCE) { + this.modules.push(mod); + } + + } else if (mod.modname == this.modName) { + this.modules.push(mod); + } + }); + }); + + // Get the handler data for the modules. + const fakeSection = { + visible: 1, + modules: this.modules + }; + this.courseHelper.addHandlerDataForModules([fakeSection], this.courseId); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting data'); + }); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.courseProvider.invalidateSections(this.courseId).finally(() => { + return this.fetchData().finally(() => { + refresher.complete(); + }); + }); + } +} diff --git a/src/addon/block/activitymodules/providers/block-handler.ts b/src/addon/block/activitymodules/providers/block-handler.ts new file mode 100644 index 000000000..df211ecec --- /dev/null +++ b/src/addon/block/activitymodules/providers/block-handler.ts @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandler, CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { AddonBlockActivityModulesComponent } from '../components/activitymodules/activitymodules'; + +/** + * Course nav handler. + */ +@Injectable() +export class AddonBlockActivityModulesHandler implements CoreBlockHandler { + name = 'AddonBlockActivityModulesHandler'; + blockName = 'activity_modules'; + + constructor() { + // Nothing to do. + } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_activitymodules.pluginname', + class: 'addon-block-activitymodules', + component: AddonBlockActivityModulesComponent + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9c4344d25..04988829d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -84,6 +84,7 @@ import { AddonCompetencyModule } from '@addon/competency/competency.module'; import { AddonCourseCompletionModule } from '@addon/coursecompletion/coursecompletion.module'; import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module'; import { AddonFilesModule } from '@addon/files/files.module'; +import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; import { AddonBlockSiteMainMenuModule } from '@addon/block/sitemainmenu/sitemainmenu.module'; import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module'; @@ -197,6 +198,7 @@ export const CORE_PROVIDERS: any[] = [ AddonCourseCompletionModule, AddonUserProfileFieldModule, AddonFilesModule, + AddonBlockActivityModulesModule, AddonBlockMyOverviewModule, AddonBlockSiteMainMenuModule, AddonBlockTimelineModule, diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 5961b72b8..16a1bc386 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -842,6 +842,16 @@ export class CoreCourseProvider { }); } + /** + * Check if a module has a view page. E.g. labels don't have a view page. + * + * @param {any} module The module object. + * @return {boolean} Whether the module has a view page. + */ + moduleHasView(module: any): boolean { + return !!module.url; + } + /** * Change the course status, setting it to the previous status. * diff --git a/src/lang/en.json b/src/lang/en.json index ebd2b9aa2..3838eab45 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -189,6 +189,7 @@ "refresh": "Refresh", "required": "Required", "requireduserdatamissing": "This user lacks some required profile data. Please enter the data in your site and try again.
{{$a}}", + "resources": "Resources", "restore": "Restore", "retry": "Retry", "save": "Save", diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 85b0bfbd5..408a4c950 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -931,10 +931,11 @@ export class CoreUtilsProvider { * @param {object} obj Object to convert. * @param {string} keyName Name of the properties where to store the keys. * @param {string} valueName Name of the properties where to store the values. - * @param {boolean} [sort] True to sort keys alphabetically, false otherwise. + * @param {boolean} [sortByKey] True to sort keys alphabetically, false otherwise. Has priority over sortByValue. + * @param {boolean} [sortByValue] True to sort values alphabetically, false otherwise. * @return {any[]} Array of objects with the name & value of each property. */ - objectToArrayOfObjects(obj: object, keyName: string, valueName: string, sort?: boolean): any[] { + objectToArrayOfObjects(obj: object, keyName: string, valueName: string, sortByKey?: boolean, sortByValue?: boolean): any[] { // Get the entries from an object or primitive value. const getEntries = (elKey, value): any[] | any => { if (typeof value == 'object') { @@ -964,9 +965,13 @@ export class CoreUtilsProvider { // "obj" will always be an object, so "entries" will always be an array. const entries = getEntries('', obj); - if (sort) { + if (sortByKey || sortByValue) { return entries.sort((a, b) => { - return a.name >= b.name ? 1 : -1; + if (sortByKey) { + return a[keyName] >= b[keyName] ? 1 : -1; + } else { + return a[valueName] >= b[valueName] ? 1 : -1; + } }); } @@ -980,7 +985,7 @@ export class CoreUtilsProvider { * @param {object[]} objects List of objects to convert. * @param {string} keyName Name of the properties where the keys are stored. * @param {string} valueName Name of the properties where the values are stored. - * @param [keyPrefix] Key prefix if neededs to delete it. + * @param {string} [keyPrefix] Key prefix if neededs to delete it. * @return {object} Object. */ objectToKeyValueMap(objects: object[], keyName: string, valueName: string, keyPrefix?: string): object { @@ -1096,6 +1101,23 @@ export class CoreUtilsProvider { } } + /** + * Given an object, sort its values. Values need to be primitive values, it cannot have subobjects. + * + * @param {object} obj The object to sort. If it isn't an object, the original value will be returned. + * @return {object} Sorted object. + */ + sortValues(obj: object): object { + if (typeof obj == 'object' && !Array.isArray(obj)) { + // It's an object, sort it. Convert it to an array to be able to sort it and then convert it back to object. + const array = this.objectToArrayOfObjects(obj, 'name', 'value', false, true); + + return this.objectToKeyValueMap(array, 'name', 'value'); + } else { + return obj; + } + } + /** * Sum the filesizes from a list of files checking if the size will be partial or totally calculated. *