From 5303d3ab23efd7a8a0e622b2617a3758dd3447bb Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 22 Dec 2017 13:29:42 +0100 Subject: [PATCH] MOBILE-2310 course: Implement course provider --- src/assets/img/mod/assign.svg | 89 ++++ src/assets/img/mod/assignment.svg | 89 ++++ src/assets/img/mod/book.svg | 80 +++ src/assets/img/mod/chat.svg | 77 +++ src/assets/img/mod/choice.svg | 46 ++ src/assets/img/mod/data.svg | 87 ++++ src/assets/img/mod/database.svg | 87 ++++ src/assets/img/mod/external-tool.svg | 55 +++ src/assets/img/mod/feedback.svg | 133 +++++ src/assets/img/mod/file.svg | 60 +++ src/assets/img/mod/folder.svg | 65 +++ src/assets/img/mod/forum.svg | 71 +++ src/assets/img/mod/glossary.svg | 146 ++++++ src/assets/img/mod/ims.svg | 156 ++++++ src/assets/img/mod/imscp.svg | 156 ++++++ src/assets/img/mod/label.svg | 94 ++++ src/assets/img/mod/lesson.svg | 126 +++++ src/assets/img/mod/lti.svg | 55 +++ src/assets/img/mod/page.svg | 112 +++++ src/assets/img/mod/quiz.svg | 90 ++++ src/assets/img/mod/resource.svg | 60 +++ src/assets/img/mod/scorm.svg | 84 ++++ src/assets/img/mod/survey.svg | 89 ++++ src/assets/img/mod/url.svg | 485 ++++++++++++++++++ src/assets/img/mod/wiki.svg | 228 +++++++++ src/assets/img/mod/workshop.svg | 98 ++++ src/core/course/lang/en.json | 22 + src/core/course/providers/course.ts | 714 +++++++++++++++++++++++++++ src/providers/events.ts | 1 + src/providers/utils/utils.ts | 16 + 30 files changed, 3671 insertions(+) create mode 100644 src/assets/img/mod/assign.svg create mode 100644 src/assets/img/mod/assignment.svg create mode 100644 src/assets/img/mod/book.svg create mode 100644 src/assets/img/mod/chat.svg create mode 100644 src/assets/img/mod/choice.svg create mode 100644 src/assets/img/mod/data.svg create mode 100644 src/assets/img/mod/database.svg create mode 100644 src/assets/img/mod/external-tool.svg create mode 100644 src/assets/img/mod/feedback.svg create mode 100644 src/assets/img/mod/file.svg create mode 100644 src/assets/img/mod/folder.svg create mode 100644 src/assets/img/mod/forum.svg create mode 100644 src/assets/img/mod/glossary.svg create mode 100644 src/assets/img/mod/ims.svg create mode 100644 src/assets/img/mod/imscp.svg create mode 100644 src/assets/img/mod/label.svg create mode 100644 src/assets/img/mod/lesson.svg create mode 100644 src/assets/img/mod/lti.svg create mode 100644 src/assets/img/mod/page.svg create mode 100644 src/assets/img/mod/quiz.svg create mode 100644 src/assets/img/mod/resource.svg create mode 100644 src/assets/img/mod/scorm.svg create mode 100644 src/assets/img/mod/survey.svg create mode 100644 src/assets/img/mod/url.svg create mode 100644 src/assets/img/mod/wiki.svg create mode 100644 src/assets/img/mod/workshop.svg create mode 100644 src/core/course/lang/en.json create mode 100644 src/core/course/providers/course.ts 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