diff --git a/src/assets/img/mod/assign.svg b/src/assets/img/mod/assign.svg
new file mode 100644
index 000000000..41a788985
--- /dev/null
+++ b/src/assets/img/mod/assign.svg
@@ -0,0 +1,89 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/assignment.svg b/src/assets/img/mod/assignment.svg
new file mode 100644
index 000000000..41a788985
--- /dev/null
+++ b/src/assets/img/mod/assignment.svg
@@ -0,0 +1,89 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/book.svg b/src/assets/img/mod/book.svg
new file mode 100644
index 000000000..740a35160
--- /dev/null
+++ b/src/assets/img/mod/book.svg
@@ -0,0 +1,80 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/chat.svg b/src/assets/img/mod/chat.svg
new file mode 100644
index 000000000..9dd304b78
--- /dev/null
+++ b/src/assets/img/mod/chat.svg
@@ -0,0 +1,77 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/choice.svg b/src/assets/img/mod/choice.svg
new file mode 100644
index 000000000..4d455910c
--- /dev/null
+++ b/src/assets/img/mod/choice.svg
@@ -0,0 +1,46 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/data.svg b/src/assets/img/mod/data.svg
new file mode 100644
index 000000000..954777f09
--- /dev/null
+++ b/src/assets/img/mod/data.svg
@@ -0,0 +1,87 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/database.svg b/src/assets/img/mod/database.svg
new file mode 100644
index 000000000..954777f09
--- /dev/null
+++ b/src/assets/img/mod/database.svg
@@ -0,0 +1,87 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/external-tool.svg b/src/assets/img/mod/external-tool.svg
new file mode 100644
index 000000000..ebbbe3084
--- /dev/null
+++ b/src/assets/img/mod/external-tool.svg
@@ -0,0 +1,55 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/feedback.svg b/src/assets/img/mod/feedback.svg
new file mode 100644
index 000000000..58d0f080b
--- /dev/null
+++ b/src/assets/img/mod/feedback.svg
@@ -0,0 +1,133 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/file.svg b/src/assets/img/mod/file.svg
new file mode 100644
index 000000000..2039a2ea2
--- /dev/null
+++ b/src/assets/img/mod/file.svg
@@ -0,0 +1,60 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/folder.svg b/src/assets/img/mod/folder.svg
new file mode 100644
index 000000000..6c2a9fe19
--- /dev/null
+++ b/src/assets/img/mod/folder.svg
@@ -0,0 +1,65 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/forum.svg b/src/assets/img/mod/forum.svg
new file mode 100644
index 000000000..aab9a8f44
--- /dev/null
+++ b/src/assets/img/mod/forum.svg
@@ -0,0 +1,71 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/glossary.svg b/src/assets/img/mod/glossary.svg
new file mode 100644
index 000000000..f330727e3
--- /dev/null
+++ b/src/assets/img/mod/glossary.svg
@@ -0,0 +1,146 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/ims.svg b/src/assets/img/mod/ims.svg
new file mode 100644
index 000000000..5589cd0c5
--- /dev/null
+++ b/src/assets/img/mod/ims.svg
@@ -0,0 +1,156 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/imscp.svg b/src/assets/img/mod/imscp.svg
new file mode 100644
index 000000000..5589cd0c5
--- /dev/null
+++ b/src/assets/img/mod/imscp.svg
@@ -0,0 +1,156 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/label.svg b/src/assets/img/mod/label.svg
new file mode 100644
index 000000000..ac232fc58
--- /dev/null
+++ b/src/assets/img/mod/label.svg
@@ -0,0 +1,94 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/lesson.svg b/src/assets/img/mod/lesson.svg
new file mode 100644
index 000000000..0a0e5dfd5
--- /dev/null
+++ b/src/assets/img/mod/lesson.svg
@@ -0,0 +1,126 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/lti.svg b/src/assets/img/mod/lti.svg
new file mode 100644
index 000000000..ebbbe3084
--- /dev/null
+++ b/src/assets/img/mod/lti.svg
@@ -0,0 +1,55 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/page.svg b/src/assets/img/mod/page.svg
new file mode 100644
index 000000000..eb7cae6c8
--- /dev/null
+++ b/src/assets/img/mod/page.svg
@@ -0,0 +1,112 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/quiz.svg b/src/assets/img/mod/quiz.svg
new file mode 100644
index 000000000..90473416f
--- /dev/null
+++ b/src/assets/img/mod/quiz.svg
@@ -0,0 +1,90 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/resource.svg b/src/assets/img/mod/resource.svg
new file mode 100644
index 000000000..2039a2ea2
--- /dev/null
+++ b/src/assets/img/mod/resource.svg
@@ -0,0 +1,60 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/scorm.svg b/src/assets/img/mod/scorm.svg
new file mode 100644
index 000000000..77891eca4
--- /dev/null
+++ b/src/assets/img/mod/scorm.svg
@@ -0,0 +1,84 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/survey.svg b/src/assets/img/mod/survey.svg
new file mode 100644
index 000000000..a97fe77ef
--- /dev/null
+++ b/src/assets/img/mod/survey.svg
@@ -0,0 +1,89 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/url.svg b/src/assets/img/mod/url.svg
new file mode 100644
index 000000000..56bdb5541
--- /dev/null
+++ b/src/assets/img/mod/url.svg
@@ -0,0 +1,485 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/wiki.svg b/src/assets/img/mod/wiki.svg
new file mode 100644
index 000000000..f3101ce19
--- /dev/null
+++ b/src/assets/img/mod/wiki.svg
@@ -0,0 +1,228 @@
+
+
+
+]>
+
diff --git a/src/assets/img/mod/workshop.svg b/src/assets/img/mod/workshop.svg
new file mode 100644
index 000000000..f466455a6
--- /dev/null
+++ b/src/assets/img/mod/workshop.svg
@@ -0,0 +1,98 @@
+
+
+
+]>
+
diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json
new file mode 100644
index 000000000..8c6cb9d35
--- /dev/null
+++ b/src/core/course/lang/en.json
@@ -0,0 +1,22 @@
+{
+ "activitydisabled": "Your organisation has disabled this activity in the mobile app.",
+ "activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.",
+ "activitynotyetviewablesiteupgradeneeded": "Your organisation's Moodle installation needs to be updated.",
+ "allsections": "All sections",
+ "askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.",
+ "confirmdeletemodulefiles": "Are you sure you want to delete these files?",
+ "confirmdownload": "You are about to download {{size}}. Are you sure you want to continue?",
+ "confirmdownloadunknownsize": "It was not possible to calculate the size of the download. Are you sure you want to continue?",
+ "confirmpartialdownloadsize": "You are about to download at least {{size}}. Are you sure you want to continue?",
+ "contents": "Contents",
+ "couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
+ "couldnotloadsections": "Could not load the sections. Please try again later.",
+ "downloadcourse": "Download course",
+ "errordownloadingcourse": "Error downloading course.",
+ "errordownloadingsection": "Error downloading section.",
+ "errorgetmodule": "Error getting activity data.",
+ "hiddenfromstudents": "Hidden from students",
+ "nocontentavailable": "No content available at the moment.",
+ "overriddennotice": "Your final grade from this activity was manually adjusted.",
+ "useactivityonbrowser": "You can still use it using your device's web browser."
+}
\ No newline at end of file
diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts
new file mode 100644
index 000000000..abc44bb22
--- /dev/null
+++ b/src/core/course/providers/course.ts
@@ -0,0 +1,714 @@
+// (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 } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreEventsProvider } from '../../../providers/events';
+import { CoreLoggerProvider } from '../../../providers/logger';
+import { CoreSitesProvider } from '../../../providers/sites';
+import { CoreTimeUtilsProvider } from '../../../providers/utils/time';
+import { CoreUtilsProvider } from '../../../providers/utils/utils';
+import { CoreSiteWSPreSets } from '../../../classes/site';
+import { CoreConstants } from '../../constants';
+
+/**
+ * Service that provides some features regarding a course.
+ */
+@Injectable()
+export class CoreCourseProvider {
+ // Variables for database.
+ protected COURSE_STATUS_TABLE = 'course_status';
+ protected courseStatusTableSchema = {
+ name: this.COURSE_STATUS_TABLE,
+ columns: [
+ {
+ name: 'id',
+ type: 'INTEGER',
+ primaryKey: true
+ },
+ {
+ name: 'status',
+ type: 'TEXT',
+ notNull: true
+ },
+ {
+ name: 'previous',
+ type: 'TEXT'
+ },
+ {
+ name: 'updated',
+ type: 'INTEGER'
+ },
+ {
+ name: 'downloadTime',
+ type: 'INTEGER'
+ },
+ {
+ name: 'previousDownloadTime',
+ type: 'INTEGER'
+ }
+ ]
+ }
+
+ protected logger;
+ protected coreModules = [
+ 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'database', 'date', 'external-tool',
+ 'feedback', 'file', 'folder', 'forum', 'glossary', 'ims', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz',
+ 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
+ ];
+
+ constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider,
+ private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private translate: TranslateService) {
+ this.logger = logger.getInstance('CoreCourseProvider');
+
+ this.sitesProvider.createTableFromSchema(this.courseStatusTableSchema);
+ }
+
+ /**
+ * Check if module completion could have changed. If it could have, trigger event. This function must be used,
+ * for example, after calling a "module_view" WS since it can change the module completion.
+ *
+ * @param {number} courseId Course ID.
+ * @param {any} completion Completion status of the module.
+ */
+ checkModuleCompletion(courseId: number, completion: any) : void {
+ if (completion && completion.tracking === 2 && completion.state === 0) {
+ this.invalidateSections(courseId).finally(() => {
+ this.eventsProvider.trigger(CoreEventsProvider.COMPLETION_MODULE_VIEWED, {courseId: courseId});
+ });
+ }
+ }
+
+ /**
+ * Clear all courses status in a site.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when all status are cleared.
+ */
+ clearAllCoursesStatus(siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ this.logger.debug('Clear all course status for site ' + site.id);
+
+ return site.getDb().deleteRecords(this.COURSE_STATUS_TABLE).then(() => {
+ this.triggerCourseStatusChanged(-1, CoreConstants.notDownloaded, site.id);
+ });
+ });
+ }
+
+ /**
+ * Get completion status of all the activities in a course for a certain user.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @param {number} [userId] User ID. If not defined, current user.
+ * @return {Promise} Promise resolved with the completion statuses: object where the key is module ID.
+ */
+ getActivitiesCompletionStatus(courseId: number, siteId?: string, userId?: number) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ userId = userId || site.getUserId();
+
+ this.logger.debug(`Getting completion status for user ${userId} in course ${courseId}`);
+
+ let params = {
+ courseid: courseId,
+ userid: userId
+ },
+ preSets = {
+ cacheKey: this.getActivitiesCompletionCacheKey(courseId, userId)
+ };
+
+ return site.read('core_completion_get_activities_completion_status', params, preSets).then((data) => {
+ if (data && data.statuses) {
+ return this.utils.arrayToObject(data.statuses, 'cmid');
+ }
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Get cache key for activities completion WS calls.
+ *
+ * @param {number} courseId Course ID.
+ * @param {number} userId User ID.
+ * @return {string} Cache key.
+ */
+ protected getActivitiesCompletionCacheKey(courseId: number, userId: number) : string {
+ return this.getRootCacheKey() + 'activitiescompletion:' + courseId + ':' + userId;
+ }
+
+ /**
+ * Get the data stored for a course.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the data.
+ */
+ getCourseStatusData(courseId: number, siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.getDb().getRecord(this.COURSE_STATUS_TABLE, {id: courseId}).then((entry) => {
+ if (!entry) {
+ return Promise.reject(null);
+ }
+ return entry;
+ });
+ });
+ }
+
+ /**
+ * Get a course status.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the status.
+ */
+ getCourseStatus(courseId: number, siteId?: string) : Promise {
+ return this.getCourseStatusData(courseId, siteId).then((entry) => {
+ return entry.status || CoreConstants.notDownloaded;
+ }).catch(() => {
+ return CoreConstants.notDownloaded;
+ });
+ }
+
+ /**
+ * Get a module from Moodle.
+ *
+ * @param {number} moduleId The module ID.
+ * @param {number} [courseId] The course ID. Recommended to speed up the process and minimize data usage.
+ * @param {number} [sectionId] The section ID.
+ * @param {boolean} [preferCache] True if shouldn't call WS if data is cached, false otherwise.
+ * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the module.
+ */
+ getModule(moduleId: number, courseId?: number, sectionId?: number, preferCache?: boolean, ignoreCache?: boolean,
+ siteId?: string) : Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ let promise;
+
+ if (!courseId) {
+ // No courseId passed, try to retrieve it.
+ promise = this.getModuleBasicInfo(moduleId, siteId).then((module) => {
+ return module.course;
+ });
+ } else {
+ promise = Promise.resolve(courseId);
+ }
+
+ return promise.then((cid) => {
+ courseId = cid;
+
+ // Get the site.
+ return this.sitesProvider.getSite(siteId);
+ }).then((site) => {
+ // We have courseId, we can use core_course_get_contents for compatibility.
+ this.logger.debug(`Getting module ${moduleId} in course ${courseId}`);
+
+ let params = {
+ courseid: courseId,
+ options: [
+ {
+ name: 'cmid',
+ value: moduleId
+ }
+ ]
+ },
+ preSets: any = {
+ cacheKey: this.getModuleCacheKey(moduleId),
+ omitExpires: preferCache
+ };
+
+ if (!preferCache && ignoreCache) {
+ preSets.getFromCache = 0;
+ preSets.emergencyCache = 0;
+ }
+
+ if (sectionId) {
+ params.options.push({
+ name: 'sectionid',
+ value: sectionId
+ });
+ }
+
+ return site.read('core_course_get_contents', params, preSets).catch(() => {
+ // Error getting the module. Try to get all contents (without filtering by module).
+ return this.getSections(courseId, false, false, preSets, siteId);
+ }).then((sections) => {
+ for (let i = 0; i < sections.length; i++) {
+ let section = sections[i];
+ for (let j = 0; j < section.modules.length; j++) {
+ let module = section.modules[j];
+ if (module.id == moduleId) {
+ module.course = courseId;
+ return module;
+ }
+ }
+ }
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Gets a module basic info by module ID.
+ *
+ * @param {number} moduleId Module ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the module's info.
+ */
+ getModuleBasicInfo(moduleId: number, siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let params = {
+ cmid: moduleId
+ },
+ preSets = {
+ cacheKey: this.getModuleCacheKey(moduleId)
+ };
+
+ return site.read('core_course_get_course_module', params, preSets).then((response) => {
+ if (response.warnings && response.warnings.length) {
+ return Promise.reject(response.warnings[0]);
+ } else if (response.cm) {
+ return response.cm;
+ }
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Gets a module basic grade info by module ID.
+ *
+ * @param {number} moduleId Module ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the module's grade info.
+ */
+ getModuleBasicGradeInfo(moduleId: number, siteId?: string) : Promise {
+ return this.getModuleBasicInfo(moduleId, siteId).then((info) => {
+ let grade = {
+ advancedgrading: info.advancedgrading || false,
+ grade: info.grade || false,
+ gradecat: info.gradecat || false,
+ gradepass: info.gradepass || false,
+ outcomes: info.outcomes || false,
+ scale: info.scale || false
+ };
+
+ if (grade.grade !== false || grade.advancedgrading !== false || grade.outcomes !== false) {
+ return grade;
+ }
+ return false;
+ });
+ }
+
+ /**
+ * Gets a module basic info by instance.
+ *
+ * @param {number} id Instance ID.
+ * @param {string} module Name of the module. E.g. 'glossary'.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the module's info.
+ */
+ getModuleBasicInfoByInstance(id: number, module: string, siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let params = {
+ instance: id,
+ module: module
+ },
+ preSets = {
+ cacheKey: this.getModuleBasicInfoByInstanceCacheKey(id, module)
+ };
+
+ return site.read('core_course_get_course_module_by_instance', params, preSets).then((response) => {
+ if (response.warnings && response.warnings.length) {
+ return Promise.reject(response.warnings[0]);
+ } else if (response.cm) {
+ return response.cm;
+ }
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Get cache key for get module by instance WS calls.
+ *
+ * @param {number} id Instance ID.
+ * @param {string} module Name of the module. E.g. 'glossary'.
+ * @return {string} Cache key.
+ */
+ protected getModuleBasicInfoByInstanceCacheKey(id: number, module: string) : string {
+ return this.getRootCacheKey() + 'moduleByInstance:' + module + ':' + id;
+ }
+
+ /**
+ * Get cache key for module WS calls.
+ *
+ * @param {number} moduleId Module ID.
+ * @return {string} Cache key.
+ */
+ protected getModuleCacheKey(moduleId: number) : string {
+ return this.getRootCacheKey() + 'module:' + moduleId;
+ }
+
+ /**
+ * Returns the source to a module icon.
+ *
+ * @param {string} moduleName The module name.
+ * @return {string} The IMG src.
+ */
+ getModuleIconSrc(moduleName: string) : string {
+ if (this.coreModules.indexOf(moduleName) < 0) {
+ moduleName = 'external-tool';
+ }
+
+ return 'assets/img/mod/' + moduleName + '.svg';
+ }
+
+ /**
+ * Get the section ID a module belongs to.
+ *
+ * @param {number} moduleId The module ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the section ID.
+ */
+ getModuleSectionId(moduleId: number, siteId?: string) : Promise {
+ // Try to get the section using getModuleBasicInfo.
+ return this.getModuleBasicInfo(moduleId, siteId).then((module) => {
+ return module.section;
+ });
+ }
+
+ /**
+ * Get the root cache key for the WS calls related to courses.
+ *
+ * @return {string} Root cache key.
+ */
+ protected getRootCacheKey() : string {
+ return 'mmCourse:';
+ }
+
+ /**
+ * Return a specific section.
+ *
+ * @param {number} courseId The course ID.
+ * @param {number} sectionId The section ID.
+ * @param {boolean} [excludeModules] Do not return modules, return only the sections structure.
+ * @param {boolean} [excludeContents] Do not return module contents (i.e: files inside a resource).
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the section.
+ */
+ getSection(courseId: number, sectionId?: number, excludeModules?: boolean, excludeContents?: boolean, siteId?: string)
+ : Promise {
+
+ if (sectionId < 0) {
+ return Promise.reject('Invalid section ID');
+ }
+
+ return this.getSections(courseId, excludeModules, excludeContents, undefined, siteId).then((sections) => {
+ for (let i = 0; i < sections.length; i++) {
+ if (sections[i].id == sectionId) {
+ return sections[i];
+ }
+ }
+
+ return Promise.reject('Unkown section');
+ });
+ }
+
+ /**
+ * Get the course sections.
+ *
+ * @param {number} courseId The course ID.
+ * @param {boolean} [excludeModules] Do not return modules, return only the sections structure.
+ * @param {boolean} [excludeContents] Do not return module contents (i.e: files inside a resource).
+ * @param {CoreSiteWSPreSets} [preSets] Presets to use.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} The reject contains the error message, else contains the sections.
+ */
+ getSections(courseId?: number, excludeModules?: boolean, excludeContents?: boolean, preSets?: CoreSiteWSPreSets,
+ siteId?: string) : Promise {
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ preSets = preSets || {};
+ preSets.cacheKey = this.getSectionsCacheKey(courseId);
+ preSets.getCacheUsingCacheKey = true; // This is to make sure users don't lose offline access when updating.
+
+ let params = {
+ courseid: courseId,
+ options: [
+ {
+ name: 'excludemodules',
+ value: excludeModules ? 1 : 0
+ },
+ {
+ name: 'excludecontents',
+ value: excludeContents ? 1 : 0
+ }
+ ]
+ };
+
+ return site.read('core_course_get_contents', params, preSets).then((sections) => {
+ let siteHomeId = site.getSiteHomeId(),
+ showSections = true;
+
+ if (courseId == siteHomeId) {
+ showSections = site.getStoredConfig('numsections');
+ }
+
+ if (typeof showSections != 'undefined' && !showSections && sections.length > 0) {
+ // Get only the last section (Main menu block section).
+ sections.pop();
+ }
+
+ return sections;
+ });
+ });
+ }
+
+ /**
+ * Get cache key for section WS call.
+ *
+ * @param {number} courseId Course ID.
+ * @return {string} Cache key.
+ */
+ protected getSectionsCacheKey(courseId) : string {
+ return this.getRootCacheKey() + 'sections:' + courseId;
+ }
+
+ /**
+ * Invalidates module WS call.
+ *
+ * @param {number} moduleId Module ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateModule(moduleId: number, siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKey(this.getModuleCacheKey(moduleId));
+ });
+ }
+
+ /**
+ * Invalidates module WS call.
+ *
+ * @param {number} id Instance ID.
+ * @param {string} module Name of the module. E.g. 'glossary'.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateModuleByInstance(id: number, module: string, siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKey(this.getModuleBasicInfoByInstanceCacheKey(id, module));
+ });
+ }
+
+ /**
+ * Invalidates sections WS call.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @param {number} [userId] User ID. If not defined, current user.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateSections(courseId: number, siteId?: string, userId?: number) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let promises = [],
+ siteHomeId = site.getSiteHomeId();
+
+ userId = userId || site.getUserId();
+
+ promises.push(site.invalidateWsCacheForKey(this.getSectionsCacheKey(courseId)));
+ promises.push(site.invalidateWsCacheForKey(this.getActivitiesCompletionCacheKey(courseId, userId)));
+ if (courseId == siteHomeId) {
+ promises.push(site.invalidateConfig());
+ }
+ return Promise.all(promises);
+ });
+ }
+
+ /**
+ * Load module contents into module.contents if they aren't loaded already.
+ *
+ * @param {any} module Module to load the contents.
+ * @param {number} [courseId] The course ID. Recommended to speed up the process and minimize data usage.
+ * @param {number} [sectionId] The section ID.
+ * @param {boolean} [preferCache] True if shouldn't call WS if data is cached, false otherwise.
+ * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when loaded.
+ */
+ loadModuleContents(module: any, courseId?: number, sectionId?: number, preferCache?: boolean, ignoreCache?: boolean,
+ siteId?: string) : Promise {
+ if (!ignoreCache && module.contents && module.contents.length) {
+ // Already loaded.
+ return Promise.resolve();
+ }
+
+ return this.getModule(module.id, courseId, sectionId, preferCache, ignoreCache, siteId).then((mod) => {
+ module.contents = mod.contents;
+ });
+ }
+
+ /**
+ * Report a course and section as being viewed.
+ *
+ * @param {number} courseId Course ID.
+ * @param {number} [sectionNumber] Section number.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the WS call is successful.
+ */
+ logView(courseId: number, sectionNumber?: number, siteId?: string) : Promise {
+ let params: any = {
+ courseid: courseId
+ };
+ if (typeof sectionNumber != 'undefined') {
+ params.sectionnumber = sectionNumber;
+ }
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.write('core_course_view_course', params).then((response) => {
+ if (!response.status) {
+ return Promise.reject(null);
+ }
+ })
+ });
+ }
+
+ /**
+ * Change the course status, setting it to the previous status.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the status is changed. Resolve param: new status.
+ */
+ setCoursePreviousStatus(courseId: number, siteId?: string) : Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ this.logger.debug(`Set previous status for course ${courseId} in site ${siteId}`);
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let db = site.getDb(),
+ newData: any = {};
+
+ // Get current stored data.
+ return this.getCourseStatusData(courseId, siteId).then((entry) => {
+ this.logger.debug(`Set previous status '${entry.status}' for course ${courseId}`);
+
+ newData.status = entry.previous || CoreConstants.notDownloaded;
+ newData.updated = Date.now();
+ if (entry.status == CoreConstants.downloading) {
+ // Going back from downloading to previous status, restore previous download time.
+ newData.downloadTime = entry.previousDownloadTime;
+ }
+
+ return db.updateRecords(this.COURSE_STATUS_TABLE, newData, {id: courseId}).then(() => {
+ // Success updating, trigger event.
+ this.triggerCourseStatusChanged(courseId, newData.status, siteId);
+ return newData.status;
+ });
+ });
+ });
+ }
+
+ /**
+ * Store course status.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} status New course status.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the status is stored.
+ */
+ setCourseStatus(courseId: number, status: string, siteId?: string) : Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId()
+
+ this.logger.debug(`Set status '${status}' for course ${courseId} in site ${siteId}`);
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let downloadTime,
+ previousDownloadTime;
+
+ if (status == CoreConstants.downloading) {
+ // Set download time if course is now downloading.
+ downloadTime = this.timeUtils.timestamp();
+ }
+
+ // Search current status to set it as previous status.
+ return this.getCourseStatusData(courseId, siteId).then((entry) => {
+ if (typeof downloadTime == 'undefined') {
+ // Keep previous download time.
+ downloadTime = entry.downloadTime;
+ previousDownloadTime = entry.previousDownloadTime;
+ } else {
+ // downloadTime will be updated, store current time as previous.
+ previousDownloadTime = entry.downloadTime;
+ }
+
+ return entry.status;
+ }).catch(() => {
+ // No previous status.
+ }).then((previousStatus) => {
+ if (previousStatus != status) {
+ // Status has changed, update it.
+ let data = {
+ id: courseId,
+ status: status,
+ previous: previousStatus,
+ updated: new Date().getTime(),
+ downloadTime: downloadTime,
+ previousDownloadTime: previousDownloadTime
+ };
+
+ return site.getDb().insertOrUpdateRecord(this.COURSE_STATUS_TABLE, data, {id: courseId});
+ }
+ }).then(() => {
+ // Success inserting, trigger event.
+ this.triggerCourseStatusChanged(courseId, status, siteId);
+ });
+ });
+ }
+
+ /**
+ * Translate a module name to current language.
+ *
+ * @param {string} moduleName The module name.
+ * @return {string} Translated name.
+ */
+ translateModuleName(moduleName: string) : string {
+ if (this.coreModules.indexOf(moduleName) < 0) {
+ moduleName = 'external-tool';
+ }
+
+ const langKey = 'core.mod_' + moduleName,
+ translated = this.translate.instant(langKey);
+
+ return translated !== langKey ? translated : moduleName;
+ }
+
+ /**
+ * Trigger mmCoreEventCourseStatusChanged with the right data.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} status New course status.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ */
+ protected triggerCourseStatusChanged(courseId: number, status: string, siteId?: string) : void {
+ this.eventsProvider.trigger(CoreEventsProvider.COURSE_STATUS_CHANGED, {
+ siteId: siteId,
+ courseId: courseId,
+ status: status
+ });
+ }
+}
diff --git a/src/providers/events.ts b/src/providers/events.ts
index 198cb6a68..c586e5925 100644
--- a/src/providers/events.ts
+++ b/src/providers/events.ts
@@ -39,6 +39,7 @@ export class CoreEventsProvider {
public static COMPLETION_MODULE_VIEWED = 'completion_module_viewed';
public static USER_DELETED = 'user_deleted';
public static PACKAGE_STATUS_CHANGED = 'package_status_changed';
+ public static COURSE_STATUS_CHANGED = 'course_status_changed';
public static SECTION_STATUS_CHANGED = 'section_status_changed';
public static REMOTE_ADDONS_LOADED = 'remote_addons_loaded';
public static LOGIN_SITE_CHECKED = 'login_site_checked';
diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts
index 4160cbca8..6283863e8 100644
--- a/src/providers/utils/utils.ts
+++ b/src/providers/utils/utils.ts
@@ -79,6 +79,22 @@ export class CoreUtilsProvider {
});
}
+ /**
+ * Converts an array of objects to an object, using a property of each entry as the key.
+ * E.g. [{id: 10, name: 'A'}, {id: 11, name: 'B'}] => {10: {id: 10, name: 'A'}, 11: {id: 11, name: 'B'}}
+ *
+ * @param {any[]} array The array to convert.
+ * @param {string} propertyName The name of the property to use as the key.
+ * @return {any} The object.
+ */
+ arrayToObject(array: any[], propertyName: string) : any {
+ let result = {};
+ array.forEach((entry) => {
+ result[entry[propertyName]] = entry;
+ });
+ return result;
+ }
+
/**
* Compare two objects. This function won't compare functions and proto properties, it's a basic compare.
* Also, this will only check if itemA's properties are in itemB with same value. This function will still