diff --git a/.gitignore b/.gitignore index be5a2b54d..14e5e7e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ e2e/build !/desktop/assets/ !/desktop/electron.js src/configconstants.ts +.moodleapp-dev-config diff --git a/.travis.yml b/.travis.yml index f7dfb6833..dde8041a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ jobs: script: scripts/aot.sh - stage: build name: "Build Android" - if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND NOT branch = desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank)) + if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch != desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank)) os: linux dist: trusty group: edge @@ -69,9 +69,9 @@ jobs: script: scripts/aot.sh - stage: build name: "Build iOS" - if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND NOT branch = desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank)) + if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch != desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank)) os: osx - osx_image: xcode11.3 + osx_image: xcode12u env: - BUILD_PLATFORM='ios' script: scripts/aot.sh @@ -88,7 +88,7 @@ jobs: name: "Build MacOS" if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch = desktop) OR (env(DEPLOY) = 3 AND tag IS NOT blank)) os: osx - osx_image: xcode11.3 + osx_image: xcode12u env: - ELECTRON_CACHE=$HOME/.cache/electron - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder diff --git a/PACKAGE_PROBLEMS.md b/PACKAGE_PROBLEMS.md index 9b193aa7e..03663b6af 100644 --- a/PACKAGE_PROBLEMS.md +++ b/PACKAGE_PROBLEMS.md @@ -13,3 +13,5 @@ jszip has problems with "lie" dependency on greater versions than 3.1 promise.prototype.finally has problems on greater versions than 3.1 +cordova-ios: should remain on 5.1 because of: https://github.com/apache/cordova-ios/pull/801 + diff --git a/config.xml b/config.xml index fe07ba0f1..01aeaa0c1 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + Moodle Moodle official app Moodle Mobile team @@ -56,7 +56,7 @@ - + @@ -112,11 +112,6 @@ - - - - - @@ -219,6 +214,14 @@ + + + + + + + + @@ -241,7 +244,7 @@ - 3.9.2 + 3.9.3 YES diff --git a/desktop/assets/mac/loginhelper.plist b/desktop/assets/mac/loginhelper.plist new file mode 100644 index 000000000..8e31f755a --- /dev/null +++ b/desktop/assets/mac/loginhelper.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/desktop/assets/mac/sign.sh b/desktop/assets/mac/sign.sh index f26b79021..ae1523be2 100755 --- a/desktop/assets/mac/sign.sh +++ b/desktop/assets/mac/sign.sh @@ -1,17 +1,26 @@ #!/bin/bash +# +# Script to sign macOSX pkg. +# https://www.electronjs.org/docs/tutorial/mac-app-store-submission-guide +# # Name of your app. APP="Moodle Desktop" -# The path of your app to sign. -APP_PATH="desktop/dist/mas/Moodle Desktop.app" -# The path to the location you want to put the signed package. -RESULT_PATH="desktop/dist/mas/$APP.pkg" # The name of certificates you requested. APP_KEY="3rd Party Mac Developer Application: Moodle Pty Ltd (2NU57U5PAW)" INSTALLER_KEY="3rd Party Mac Developer Installer: Moodle Pty Ltd (2NU57U5PAW)" + + +BASEPATH="desktop/dist/mas" +# The path of your app to sign. +APP_PATH="${BASEPATH}/${APP}.app" +# The path to the location you want to put the signed package. +RESULT_PATH="${BASEPATH}/${APP}.pkg" + # The path of your plist files. CHILD_PLIST="desktop/assets/mac/child.plist" PARENT_PLIST="desktop/assets/mac/parent.plist" +LOGINHELPER_PLIST="desktop/assets/mac/loginhelper.plist" FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks" @@ -21,11 +30,9 @@ codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electr codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework" codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/Contents/MacOS/$APP Helper" codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/Contents/MacOS/$APP Helper EH" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper NP.app/Contents/MacOS/$APP Helper NP" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper NP.app/" +codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/Contents/MacOS/$APP Login Helper" +codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/" codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP" codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH" -productbuild --component "$APP_PATH" /Applications --sign "$INSTALLER_KEY" "$RESULT_PATH" \ No newline at end of file +productbuild --component "$APP_PATH" /Applications --sign "$INSTALLER_KEY" "$RESULT_PATH" diff --git a/desktop/assets/windows/AppXManifest.xml b/desktop/assets/windows/AppXManifest.xml index 3aa37e33a..c74afc756 100644 --- a/desktop/assets/windows/AppXManifest.xml +++ b/desktop/assets/windows/AppXManifest.xml @@ -6,7 +6,7 @@ + Version="3.9.3.0" /> Moodle Desktop Moodle Pty Ltd. diff --git a/gulp/dev-config.js b/gulp/dev-config.js new file mode 100644 index 000000000..b270bf51f --- /dev/null +++ b/gulp/dev-config.js @@ -0,0 +1,69 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); + +const DEV_CONFIG_FILE = '.moodleapp-dev-config'; + +/** + * Class to read and write dev-config data from a file. + */ +class DevConfig { + + constructor() { + this.loadFileData(); + } + + /** + * Get a setting. + * + * @param name Name of the setting to get. + * @param defaultValue Value to use if not found. + */ + get(name, defaultValue) { + return typeof this.config[name] != 'undefined' ? this.config[name] : defaultValue; + } + + /** + * Load file data to memory. + */ + loadFileData() { + if (!fs.existsSync(DEV_CONFIG_FILE)) { + this.config = {}; + + return; + } + + try { + this.config = JSON.parse(fs.readFileSync(DEV_CONFIG_FILE)); + } catch (error) { + console.error('Error reading dev config file.', error); + this.config = {}; + } + } + + /** + * Save some settings. + * + * @param settings Object with the settings to save. + */ + save(settings) { + this.config = Object.assign(this.config, settings); + + // Save the data in the dev file. + fs.writeFileSync(DEV_CONFIG_FILE, JSON.stringify(this.config, null, 4)); + } +} + +module.exports = new DevConfig(); diff --git a/gulp/git.js b/gulp/git.js new file mode 100644 index 000000000..f3f66298a --- /dev/null +++ b/gulp/git.js @@ -0,0 +1,237 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const exec = require('child_process').exec; +const fs = require('fs'); +const DevConfig = require('./dev-config'); +const Utils = require('./utils'); + +/** + * Class to run git commands. + */ +class Git { + + /** + * Create a patch. + * + * @param range Show only commits in the specified revision range. + * @param saveTo Path to the file to save the patch to. If not defined, the patch contents will be returned. + * @return Promise resolved when done. If saveTo not provided, it will return the patch contents. + */ + createPatch(range, saveTo) { + return new Promise((resolve, reject) => { + exec(`git format-patch ${range} --stdout`, (err, result) => { + if (err) { + reject(err || 'Cannot create patch.'); + return; + } + + if (!saveTo) { + resolve(result); + return; + } + + // Save it to a file. + const directory = saveTo.substring(0, saveTo.lastIndexOf('/')); + if (directory && directory != '.' && directory != '..' && !fs.existsSync(directory)) { + fs.mkdirSync(directory); + } + fs.writeFileSync(saveTo, result); + + resolve(); + }); + }); + } + + /** + * Get current branch. + * + * @return Promise resolved with the branch name. + */ + getCurrentBranch() { + return new Promise((resolve, reject) => { + exec('git branch --show-current', (err, branch) => { + if (branch) { + resolve(branch.replace('\n', '')); + } else { + reject (err || 'Current branch not found.'); + } + }); + }); + } + + /** + * Get the HEAD commit for a certain branch. + * + * @param branch Name of the branch. + * @param branchData Parsed branch data. If not provided it will be calculated. + * @return HEAD commit. + */ + async getHeadCommit(branch, branchData) { + if (!branchData) { + // Parse the branch to get the project and issue number. + branchData = Utils.parseBranch(branch); + } + + // Loop over the last commits to find the first commit messages that doesn't belong to the issue. + const commitsString = await this.log(50, branch, '%s_____%H'); + const commits = commitsString.split('\n'); + commits.pop(); // Remove last element, it's an empty string. + + for (let i = 0; i < commits.length; i++) { + const commit = commits[i]; + const match = Utils.getIssueFromCommitMessage(commit) == branchData.issue; + + if (i === 0 && !match) { + // Most recent commit doesn't belong to the issue. Stop looking. + break; + } + + if (!match) { + // The commit does not match any more, we found it! + return commit.split('_____')[1]; + } + } + + // Couldn't find the commit using the commit names, get the last commit in the integration branch. + const remote = DevConfig.get('upstreamRemote', 'origin'); + console.log(`Head commit not found using commit messages. Get last commit from ${remote}/integration`); + const hashes = await this.hashes(1, `${remote}/integration`); + + return hashes[0]; + } + + /** + * Get the URL of a certain remote. + * + * @param remote Remote name. + * @return Promise resolved with the remote URL. + */ + getRemoteUrl(remote) { + return new Promise((resolve, reject) => { + exec(`git remote get-url ${remote}`, (err, url) => { + if (url) { + resolve(url.replace('\n', '')); + } else { + reject (err || 'Remote not found.'); + } + }); + }); + } + + /** + * Return the latest hashes from git log. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param format Pretty-print the contents of the commit logs in a given format. + * @return Promise resolved with the list of hashes. + */ + async hashes(count, range, format) { + format = format || '%H'; + + const hashList = await this.log(count, range, format); + + const hashes = hashList.split('\n'); + hashes.pop(); // Remove last element, it's an empty string. + + return hashes; + } + + /** + * Calls the log command and returns the raw output. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param format Pretty-print the contents of the commit logs in a given format. + * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. + * @return Promise resolved with the result. + */ + log(count, range, format, path) { + if (typeof count == 'undefined') { + count = 10; + } + + let command = 'git log'; + + if (count > 0) { + command += ` -n ${count} `; + } + if (format) { + command += ` --format=${format} `; + } + if (range){ + command += ` ${range} `; + } + if (path) { + command += ` -- ${path}`; + } + + return new Promise((resolve, reject) => { + exec(command, (err, result, stderr) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); + } + + /** + * Return the latest titles of the commit messages. + * + * @param count Number of commits to display. + * @param range Show only commits in the specified revision range. + * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. + * @return Promise resolved with the list of titles. + */ + async messages(count, range, path) { + count = typeof count != 'undefined' ? count : 10; + + const messageList = await this.log(count, range, '%s', path); + + const messages = messageList.split('\n'); + messages.pop(); // Remove last element, it's an empty string. + + return messages; + } + + /** + * Push a branch. + * + * @param remote Remote to use. + * @param branch Branch to push. + * @param force Whether to force the push. + * @return Promise resolved when done. + */ + push(remote, branch, force) { + return new Promise((resolve, reject) => { + let command = `git push ${remote} ${branch}`; + if (force) { + command += ' -f'; + } + + exec(command, (err, result, stderr) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } +} + +module.exports = new Git(); diff --git a/gulp/jira.js b/gulp/jira.js new file mode 100644 index 000000000..90aeaa1c7 --- /dev/null +++ b/gulp/jira.js @@ -0,0 +1,475 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const exec = require('child_process').exec; +const https = require('https'); +const inquirer = require('inquirer'); +const fs = require('fs'); +const request = require('request'); // This lib is deprecated, but it was the only way I found to make upload files work. +const DevConfig = require('./dev-config'); +const Git = require('./git'); +const Url = require('./url'); +const Utils = require('./utils'); + +const apiVersion = 2; + +/** + * Class to interact with Jira. + */ +class Jira { + + /** + * Ask the password to the user. + * + * @return Promise resolved with the password. + */ + async askPassword() { + const data = await inquirer.prompt([ + { + type: 'password', + name: 'password', + message: `Please enter the password for the username ${this.username}.`, + }, + ]); + + return data.password; + } + + /** + * Ask the user the tracker data. + * + * @return Promise resolved with the data, rejected if cannot get. + */ + async askTrackerData() { + const data = await inquirer.prompt([ + { + type: 'input', + name: 'url', + message: 'Please enter the tracker URL.', + default: 'https://tracker.moodle.org/', + }, + { + type: 'input', + name: 'username', + message: 'Please enter your tracker username.', + }, + ]); + + DevConfig.save({ + 'tracker.url': data.url, + 'tracker.username': data.username, + }); + + return data; + } + + /** + * Build URL to perform requests to Jira. + * + * @param uri URI to add the the Jira URL. + * @return URL. + */ + buildRequestUrl(uri) { + return Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]); + } + + /** + * Delete an attachment. + * + * @param attachmentId Attachment ID. + * @return Promise resolved when done. + */ + async deleteAttachment(attachmentId) { + const response = await this.request(`attachment/${attachmentId}`, 'DELETE'); + + if (response.status != 204) { + throw new Error('Could not delete the attachment'); + } + } + + /** + * Load the issue info from jira server using a REST API call. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param fields Fields to get. + * @return Promise resolved with the issue data. + */ + async getIssue(key, fields) { + fields = fields || '*all,-comment'; + + await this.init(); // Initialize data if needed. + + const response = await this.request(`issue/${key}`, 'GET', {'fields': fields, 'expand': 'names'}); + + if (response.status == 404) { + throw new Error('Issue could not be found.'); + } else if (response.status != 200) { + throw new Error('The tracker is not available.') + } + + const issue = response.data; + issue.named = {}; + + // Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID. + const nameList = issue.names || {}; + for (const fieldKey in issue.fields) { + if (nameList[fieldKey]) { + issue.named[nameList[fieldKey]] = issue.fields[fieldKey]; + } + } + + return issue + } + + /** + * Load the version info from the jira server using a rest api call. + * + * @return Promise resolved when done. + */ + async getServerInfo() { + const response = await this.request('serverInfo'); + + if (response.status != 200) { + throw new Error(`Unexpected response code: ${response.status}`, response); + } + + this.version = response.data; + } + + /** + * Get tracker data to push an issue. + * + * @return Promise resolved with the data. + */ + async getTrackerData() { + // Check dev-config file first. + let data = this.getTrackerDataFromDevConfig(); + + if (data) { + console.log('Using tracker data from dev-config file'); + return data; + } + + // Try to use mdk now. + try { + data = await this.getTrackerDataFromMdk(); + + console.log('Using tracker data from mdk'); + + return data; + } catch (error) { + // MDK not available or not configured. Ask for the data. + const data = await this.askTrackerData(); + + data.fromInput = true; + + return data; + } + } + + /** + * Get tracker data from dev config file. + * + * @return Data, undefined if cannot get. + */ + getTrackerDataFromDevConfig() { + const url = DevConfig.get('tracker.url'); + const username = DevConfig.get('tracker.username'); + + if (url && username) { + return { + url, + username, + }; + } + } + + /** + * Get tracker URL and username from mdk. + * + * @return Promise resolved with the data, rejected if cannot get. + */ + getTrackerDataFromMdk() { + return new Promise((resolve, reject) => { + exec('mdk config show tracker.url', (err, url) => { + if (!url) { + reject(err || 'URL not found.'); + return; + } + + exec('mdk config show tracker.username', (err, username) => { + if (username) { + resolve({ + url: url.replace('\n', ''), + username: username.replace('\n', ''), + }); + } else { + reject(err | 'Username not found.'); + } + }); + }); + }); + } + + /** + * Initialize some data. + * + * @return Promise resolved when done. + */ + async init() { + if (this.initialized) { + // Already initialized. + return; + } + + // Get tracker URL and username. + const trackerData = await this.getTrackerData(); + + this.url = trackerData.url; + this.username = trackerData.username; + + const parsed = Url.parse(this.url); + this.ssl = parsed.protocol == 'https'; + this.host = parsed.domain; + this.uri = parsed.path; + + // Get the password. + const keytar = require('keytar'); + + this.password = await keytar.getPassword('mdk-jira-password', this.username); // Use same service name as mdk. + + if (!this.password) { + // Ask the user. + this.password = await this.askPassword(); + } + + while (!this.initialized) { + try { + await this.getServerInfo(); + + this.initialized = true; + keytar.setPassword('mdk-jira-password', this.username, this.password); + } catch (error) { + console.log('Error connecting to the server. Please make sure you entered the data correctly.', error); + if (trackerData.fromInput) { + // User entered the data manually, ask him again. + trackerData = await this.askTrackerData(); + + this.url = trackerData.url; + this.username = trackerData.username; + } + + this.password = await this.askPassword(); + } + } + } + + /** + * Check if a certain issue could be a security issue. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @return Promise resolved with boolean: whether it's a security issue. + */ + async isSecurityIssue(key) { + const issue = await this.getIssue(key, 'security'); + + return issue.fields && !!issue.fields.security; + } + + /** + * Sends a request to the server and returns the data. + * + * @param uri URI to add the the Jira URL. + * @param method Method to use. Defaults to 'GET'. + * @param params Params to send as GET params (in the URL). + * @param data JSON string with the data to send as POST/PUT params. + * @param headers Headers to send. + * @return Promise resolved with the result. + */ + request(uri, method, params, data, headers) { + uri = uri || ''; + method = (method || 'GET').toUpperCase(); + data = data || ''; + params = params || {}; + headers = headers || {}; + headers['Content-Type'] = 'application/json'; + + return new Promise((resolve, reject) => { + + // Build the request URL. + const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params); + + // Initialize the request. + const options = { + method: method, + auth: `${this.username}:${this.password}`, + headers: headers, + }; + const request = https.request(url, options); + + // Add data. + if (data) { + request.write(data); + } + + // Treat response. + request.on('response', (response) => { + // Read the result. + let result = ''; + response.on('data', (chunk) => { + result += chunk; + }); + response.on('end', () => { + try { + result = JSON.parse(result); + } catch (error) { + // Leave it as text. + } + + resolve({ + status: response.statusCode, + data: result, + }); + }); + }); + + request.on('error', (e) => { + reject(e); + }); + + // Send the request. + request.end(); + }); + } + + /** + * Sets a set of fields for a certain issue in Jira. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param updates Object with the fields to update. + * @return Promise resolved when done. + */ + async setCustomFields(key, updates) { + const issue = await this.getIssue(key); + const update = {'fields': {}}; + + // Detect which fields have changed. + for (const updateName in updates) { + const updateValue = updates[updateName]; + const remoteValue = issue.named[updateName]; + + if (!remoteValue || remoteValue != updateValue) { + // Map the label of the field with the field code. + let fieldKey; + for (const key in issue.names) { + if (issue.names[key] == updateName) { + fieldKey = key; + break; + } + } + + if (!fieldKey) { + throw new Error(`Could not find the field named ${updateName}.`); + } + + update.fields[fieldKey] = updateValue; + } + } + + if (!Object.keys(update.fields).length) { + // No fields to update. + console.log('No updates required.') + return; + } + + const response = await this.request(`issue/${key}`, 'PUT', null, JSON.stringify(update)); + + if (response.status != 204) { + throw new Error(`Issue was not updated: ${response.status}`, response.data); + } + + console.log('Issue updated successfully.'); + } + + /** + * Upload a new attachment to an issue. + * + * @param key Key to identify the issue. E.g. MOBILE-1234. + * @param filePath Path to the file to upload. + * @return Promise resolved when done. + */ + async upload(key, filePath) { + + const uri = `issue/${key}/attachments`; + const headers = { + 'X-Atlassian-Token': 'nocheck', + } + + const response = await this.uploadFile(uri, 'file', filePath, headers); + + if (response.status != 200) { + throw new Error('Could not upload file to Jira issue'); + } + + console.log('File successfully uploaded.') + } + + /** + * Upload a file to Jira. + * + * @param uri URI to add the the Jira URL. + * @param fieldName Name of the form field where to put the file. + * @param filePath Path to the file. + * @param headers Headers. + * @return Promise resolved with the result. + */ + async uploadFile(uri, fieldName, filePath, headers) { + uri = uri || ''; + headers = headers || {}; + headers['Content-Type'] = 'multipart/form-data'; + + return new Promise((resolve, reject) => { + // Add the file to the form data. + const formData = {}; + formData[fieldName] = { + value: fs.createReadStream(filePath), + options: { + filename: filePath.substr(filePath.lastIndexOf('/') + 1), + contentType: 'multipart/form-data', + }, + }; + + // Perform the request. + const options = { + url: this.buildRequestUrl(uri), + method: 'POST', + headers: headers, + auth: { + user: this.username, + pass: this.password, + }, + formData: formData, + }; + + request(options, (err, httpResponse, body) => { + resolve({ + status: httpResponse.statusCode, + data: body, + }); + }); + }); + } +} + +module.exports = new Jira(); diff --git a/gulp/task-build-config.js b/gulp/task-build-config.js new file mode 100644 index 000000000..69bf71aa3 --- /dev/null +++ b/gulp/task-build-config.js @@ -0,0 +1,138 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const through = require('through'); +const bufferFrom = require('buffer-from'); +const rename = require('gulp-rename'); +const exec = require('child_process').exec; + +const LICENSE = '' + + '// (C) Copyright 2015 Moodle Pty Ltd.\n' + + '//\n' + + '// Licensed under the Apache License, Version 2.0 (the "License");\n' + + '// you may not use this file except in compliance with the License.\n' + + '// You may obtain a copy of the License at\n' + + '//\n' + + '// http://www.apache.org/licenses/LICENSE-2.0\n' + + '//\n' + + '// Unless required by applicable law or agreed to in writing, software\n' + + '// distributed under the License is distributed on an "AS IS" BASIS,\n' + + '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' + + '// See the License for the specific language governing permissions and\n' + + '// limitations under the License.\n\n'; + +/** + * Task to convert config.json into a TypeScript class. + */ +class BuildConfigTask { + + /** + * Run the task. + * + * @param path Path to the config file. + * @param done Function to call when done. + */ + run(path, done) { + const self = this; + + // Get the last commit. + exec('git log -1 --pretty=format:"%H"', (err, commit, stderr) => { + if (err) { + console.error('An error occurred while getting the last commit: ' + err); + } else if (stderr) { + console.error('An error occurred while getting the last commit: ' + stderr); + } + + gulp.src(path) + .pipe(through(function(file) { + // Convert the contents of the file into a TypeScript class. + // Disable the rule variable-name in the file. + const config = JSON.parse(file.contents.toString()); + let contents = LICENSE + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n'; + + for (let key in config) { + let value = self.transformValue(config[key]); + + if (typeof config[key] != 'number' && typeof config[key] != 'boolean' && typeof config[key] != 'string') { + key = key + ': any'; + } + + // If key has quotation marks, remove them. + if (key[0] == '"') { + key = key.substr(1, key.length - 2); + } + contents += ' static ' + key + ' = ' + value + ';\n'; + } + + // Add compilation info. + contents += ' static compilationtime = ' + Date.now() + ';\n'; + contents += ' static lastcommit = \'' + commit + '\';\n'; + + contents += '}\n'; + + file.contents = bufferFrom(contents); + + this.emit('data', file); + })) + .pipe(rename('configconstants.ts')) + .pipe(gulp.dest('./src')) + .on('end', done); + }); + } + + + /** + * Recursively transform a config value into personalized TS. + * + * @param value Value to convert + * @return Converted value. + */ + transformValue(value) { + if (typeof value == 'string') { + // Wrap the string in ' and escape them. + return "'" + value.replace(/([^\\])'/g, "$1\\'") + "'"; + } + + if (typeof value != 'number' && typeof value != 'boolean') { + const isArray = Array.isArray(value); + let contents = ''; + + let quoteKeys = false; + if (!isArray) { + for (let key in value) { + if (key.indexOf('-') >= 0) { + quoteKeys = true; + break; + } + } + } + + for (let key in value) { + value[key] = this.transformValue(value[key]); + + const quotedKey = quoteKeys ? "'" + key + "'" : key; + contents += ' ' + (isArray ? '' : quotedKey + ': ') + value[key] + ",\n"; + } + + contents += (isArray ? ']' : '}'); + + return (isArray ? '[' : '{') + "\n" + contents.replace(/^/gm, ' '); + } + + return value; + } +} + +module.exports = BuildConfigTask; diff --git a/gulp/task-build-lang.js b/gulp/task-build-lang.js new file mode 100644 index 000000000..daa5d3126 --- /dev/null +++ b/gulp/task-build-lang.js @@ -0,0 +1,176 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const slash = require('gulp-slash'); +const clipEmptyFiles = require('gulp-clip-empty-files'); +const through = require('through'); +const bufferFrom = require('buffer-from'); +const File = require('vinyl'); +const pathLib = require('path'); + +/** + * Task to build the language files into a single file per language. + */ +class BuildLangTask { + + /** + * Copy a property from one object to another, adding a prefix to the key if needed. + * + * @param target Object to copy the properties to. + * @param source Object to copy the properties from. + * @param prefix Prefix to add to the keys. + */ + addProperties(target, source, prefix) { + for (let property in source) { + target[prefix + property] = source[property]; + } + } + + /** + * Run the task. + * + * @param language Language to treat. + * @param langPaths Paths to the possible language files. + * @param done Function to call when done. + */ + run(language, langPaths, done) { + const filename = language + '.json'; + const data = {}; + let firstFile = null; + const self = this; + + const paths = langPaths.map((path) => { + if (path.slice(-1) != '/') { + path = path + '/'; + } + + return path + language + '.json'; + }); + + gulp.src(paths, { allowEmpty: true }) + .pipe(slash()) + .pipe(clipEmptyFiles()) + .pipe(through(function(file) { + if (!firstFile) { + firstFile = file; + } + + return self.treatFile(file, data); + }, function() { + /* This implementation is based on gulp-jsoncombine module. + * https://github.com/reflog/gulp-jsoncombine */ + if (firstFile) { + const joinedPath = pathLib.join(firstFile.base, language + '.json'); + + const joinedFile = new File({ + cwd: firstFile.cwd, + base: firstFile.base, + path: joinedPath, + contents: self.treatMergedData(data), + }); + + this.emit('data', joinedFile); + } + + this.emit('end'); + })) + .pipe(gulp.dest(pathLib.join('./src/assets', 'lang'))) + .on('end', done); + } + + /** + * Treats a file to merge JSONs. This function is based on gulp-jsoncombine module. + * https://github.com/reflog/gulp-jsoncombine + * + * @param file File treated. + * @param data Object where to store the data. + */ + treatFile(file, data) { + if (file.isNull() || file.isStream()) { + return; // ignore + } + + try { + let srcPos = file.path.lastIndexOf('/src/'); + if (srcPos == -1) { + // It's probably a Windows environment. + srcPos = file.path.lastIndexOf('\\src\\'); + } + + const path = file.path.substr(srcPos + 5); + data[path] = JSON.parse(file.contents.toString()); + } catch (err) { + console.log('Error parsing JSON: ' + err); + } + } + + /** + * Treats the merged JSON data, adding prefixes depending on the component. + * + * @param data Merged data. + * @return Buffer with the treated data. + */ + treatMergedData(data) { + const merged = {}; + const mergedOrdered = {}; + + for (let filepath in data) { + const pathSplit = filepath.split(/[\/\\]/); + let prefix; + + pathSplit.pop(); + + switch (pathSplit[0]) { + case 'lang': + prefix = 'core'; + break; + case 'core': + if (pathSplit[1] == 'lang') { + // Not used right now. + prefix = 'core'; + } else { + prefix = 'core.' + pathSplit[1]; + } + break; + case 'addon': + // Remove final item 'lang'. + pathSplit.pop(); + // Remove first item 'addon'. + pathSplit.shift(); + + // For subplugins. We'll use plugin_subfolder_subfolder2_... + // E.g. 'mod_assign_feedback_comments'. + prefix = 'addon.' + pathSplit.join('_'); + break; + case 'assets': + prefix = 'assets.' + pathSplit[1]; + break; + } + + if (prefix) { + this.addProperties(merged, data[filepath], prefix + '.'); + } + } + + // Force ordering by string key. + Object.keys(merged).sort().forEach((key) => { + mergedOrdered[key] = merged[key]; + }); + + return bufferFrom(JSON.stringify(mergedOrdered, null, 4)); + } +} + +module.exports = BuildLangTask; diff --git a/gulp/task-combine-scss.js b/gulp/task-combine-scss.js new file mode 100644 index 000000000..0f0a28003 --- /dev/null +++ b/gulp/task-combine-scss.js @@ -0,0 +1,164 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const through = require('through'); +const bufferFrom = require('buffer-from'); +const concat = require('gulp-concat'); +const pathLib = require('path'); +const fs = require('fs'); + +/** + * Task to combine scss into a single file. + */ +class CombineScssTask { + + /** + * Finds the file and returns its content. + * + * @param capture Import file path. + * @param baseDir Directory where the file was found. + * @param paths Alternative paths where to find the imports. + * @param parsedFiles Already parsed files to reduce size of the result. + * @return Partially combined scss. + */ + getReplace(capture, baseDir, paths, parsedFiles) { + let parse = pathLib.parse(pathLib.resolve(baseDir, capture + '.scss')); + let file = parse.dir + '/' + parse.name; + + if (file.slice(-3) === '.wp') { + console.log('Windows Phone not supported "' + capture); + // File was already parsed, leave the import commented. + return '// @import "' + capture + '";'; + } + + if (!fs.existsSync(file + '.scss')) { + // File not found, might be a partial file. + file = parse.dir + '/_' + parse.name; + } + + // If file still not found, try to find the file in the alternative paths. + let x = 0; + while (!fs.existsSync(file + '.scss') && paths.length > x) { + parse = pathLib.parse(pathLib.resolve(paths[x], capture + '.scss')); + file = parse.dir + '/' + parse.name; + + x++; + } + + file = file + '.scss'; + + if (!fs.existsSync(file)) { + // File not found. Leave the import there. + console.log('File "' + capture + '" not found'); + return '@import "' + capture + '";'; + } + + if (parsedFiles.indexOf(file) >= 0) { + console.log('File "' + capture + '" already parsed'); + // File was already parsed, leave the import commented. + return '// @import "' + capture + '";'; + } + + parsedFiles.push(file); + const text = fs.readFileSync(file); + + // Recursive call. + return this.scssCombine(text, parse.dir, paths, parsedFiles); + } + + /** + * Run the task. + * + * @param done Function to call when done. + */ + run(done) { + const paths = [ + 'node_modules/ionic-angular/themes/', + 'node_modules/font-awesome/scss/', + 'node_modules/ionicons/dist/scss/' + ]; + const parsedFiles = []; + const self = this; + + gulp.src([ + './src/theme/variables.scss', + './node_modules/ionic-angular/themes/ionic.globals.*.scss', + './node_modules/ionic-angular/themes/ionic.components.scss', + './src/**/*.scss', + ]).pipe(through(function(file) { // Combine them based on @import and save it to stream. + if (file.isNull()) { + return; + } + + parsedFiles.push(file); + file.contents = bufferFrom(self.scssCombine( + file.contents, pathLib.dirname(file.path), paths, parsedFiles)); + + this.emit('data', file); + })).pipe(concat('combined.scss')) // Concat the stream output in single file. + .pipe(gulp.dest('.')) // Save file to destination. + .on('end', done); + } + + /** + * Combine scss files with its imports + * + * @param content Scss string to treat. + * @param baseDir Directory where the file was found. + * @param paths Alternative paths where to find the imports. + * @param parsedFiles Already parsed files to reduce size of the result. + * @return Scss string with the replaces done. + */ + scssCombine(content, baseDir, paths, parsedFiles) { + // Content is a Buffer, convert to string. + if (typeof content != "string") { + content = content.toString(); + } + + // Search of single imports. + let regex = /@import[ ]*['"](.*)['"][ ]*;/g; + + if (regex.test(content)) { + return content.replace(regex, (m, capture) => { + if (capture == "bmma") { + return m; + } + + return this.getReplace(capture, baseDir, paths, parsedFiles); + }); + } + + // Search of multiple imports. + regex = /@import(?:[ \n]+['"](.*)['"][,]?[ \n]*)+;/gm; + if (regex.test(content)) { + return content.replace(regex, (m, capture) => { + let text = ''; + + // Divide the import into multiple files. + const captures = m.match(/['"]([^'"]*)['"]/g); + + for (let x in captures) { + text += this.getReplace(captures[x].replace(/['"]+/g, ''), baseDir, paths, parsedFiles) + '\n'; + } + + return text; + }); + } + + return content; + } +} + +module.exports = CombineScssTask; diff --git a/gulp/task-copy-component-templates.js b/gulp/task-copy-component-templates.js new file mode 100644 index 000000000..2773a07b7 --- /dev/null +++ b/gulp/task-copy-component-templates.js @@ -0,0 +1,79 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); +const gulp = require('gulp'); +const flatten = require('gulp-flatten'); +const htmlmin = require('gulp-htmlmin'); +const pathLib = require('path'); + +const TEMPLATES_SRC = [ + './src/components/**/*.html', + './src/core/**/components/**/*.html', + './src/core/**/component/**/*.html', + // Copy all addon components because any component can be injected using extraImports. + './src/addon/**/components/**/*.html', + './src/addon/**/component/**/*.html' +]; +const TEMPLATES_DEST = './www/templates'; + +/** + * Task to copy component templates to www to make compile-html work in AOT. + */ +class CopyComponentTemplatesTask { + + /** + * Delete a folder and all its contents. + * + * @param path [description] + * @return {[type]} [description] + */ + deleteFolderRecursive(path) { + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach((file) => { + var curPath = pathLib.join(path, file); + + if (fs.lstatSync(curPath).isDirectory()) { + this.deleteFolderRecursive(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + + fs.rmdirSync(path); + } + } + + /** + * Run the task. + * + * @param done Callback to call once done. + */ + run(done) { + this.deleteFolderRecursive(TEMPLATES_DEST); + + gulp.src(TEMPLATES_SRC, { allowEmpty: true }) + .pipe(flatten()) + // Check options here: https://github.com/kangax/html-minifier + .pipe(htmlmin({ + collapseWhitespace: true, + removeComments: true, + caseSensitive: true + })) + .pipe(gulp.dest(TEMPLATES_DEST)) + .on('end', done); + } +} + +module.exports = CopyComponentTemplatesTask; diff --git a/gulp/task-push.js b/gulp/task-push.js new file mode 100644 index 000000000..54d151dde --- /dev/null +++ b/gulp/task-push.js @@ -0,0 +1,280 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const gulp = require('gulp'); +const inquirer = require('inquirer'); +const DevConfig = require('./dev-config'); +const Git = require('./git'); +const Jira = require('./jira'); +const Utils = require('./utils'); + +/** + * Task to push a git branch and update tracker issue. + */ +class PushTask { + + /** + * Ask the user whether he wants to continue. + * + * @return Promise resolved with boolean: true if he wants to continue. + */ + async askConfirmContinue() { + const answer = await inquirer.prompt([ + { + type: 'input', + name: 'confirm', + message: 'Are you sure you want to continue?', + default: 'n', + }, + ]); + + return answer.confirm == 'y'; + } + + /** + * Push a patch to the tracker and remove the previous one. + * + * @param branch Branch name. + * @param branchData Parsed branch data. + * @param remote Remote used. + * @return Promise resolved when done. + */ + async pushPatch(branch, branchData, remote) { + const headCommit = await Git.getHeadCommit(branch, branchData); + + if (!headCommit) { + throw new Error('Head commit not resolved, abort pushing patch.'); + } + + // Create the patch file. + const fileName = branch + '.patch'; + const tmpPatchPath = `./tmp/${fileName}`; + + await Git.createPatch(`${headCommit}...${branch}`, tmpPatchPath); + console.log('Git patch created'); + + // Check if there is an attachment with same name in the issue. + const issue = await Jira.getIssue(branchData.issue, 'attachment'); + + let existingAttachmentId; + const attachments = (issue.fields && issue.fields.attachment) || []; + for (const i in attachments) { + if (attachments[i].filename == fileName) { + // Found an existing attachment with the same name, we keep track of it. + existingAttachmentId = attachments[i].id; + break + } + } + + // Push the patch to the tracker. + console.log(`Uploading patch ${fileName} to the tracker...`); + await Jira.upload(branchData.issue, tmpPatchPath); + + if (existingAttachmentId) { + // On success, deleting file that was there before. + try { + console.log('Deleting older patch...') + await Jira.deleteAttachment(existingAttachmentId); + } catch (error) { + console.log('Could not delete older attachment.'); + } + } + } + + /** + * Run the task. + * + * @param args Command line arguments. + * @param done Function to call when done. + */ + async run(args, done) { + try { + const remote = args.remote || DevConfig.get('upstreamRemote', 'origin'); + let branch = args.branch; + const force = !!args.force; + + if (!branch) { + branch = await Git.getCurrentBranch(); + } + + if (!branch) { + throw new Error('Cannot determine the current branch. Please make sure youu aren\'t in detached HEAD state'); + } else if (branch == 'HEAD') { + throw new Error('Cannot push HEAD branch'); + } + + // Parse the branch to get the project and issue number. + const branchData = Utils.parseBranch(branch); + const keepRunning = await this.validateCommitMessages(branchData); + + if (!keepRunning) { + // Last commit not valid, stop. + console.log('Exiting...'); + done(); + return; + } + + if (!args.patch) { + // Check if it's a security issue to force patch mode. + try { + args.patch = await Jira.isSecurityIssue(branchData.issue); + + if (args.patch) { + console.log(`${branchData.issue} appears to be a security issue, switching to patch mode...`); + } + } catch (error) { + console.log(`Could not check if ${branchData.issue} is a security issue.`); + } + } + + if (args.patch) { + // Create and upload a patch file. + await this.pushPatch(branch, branchData, remote); + } else { + // Push the branch. + console.log(`Pushing branch ${branch} to remote ${remote}...`); + await Git.push(remote, branch, force); + + // Update tracker info. + console.log(`Branch pushed, update tracker info...`); + await this.updateTrackerGitInfo(branch, branchData, remote); + } + } catch (error) { + console.error(error); + } + + done(); + } + + /** + * Update git info in the tracker issue. + * + * @param branch Branch name. + * @param branchData Parsed branch data. + * @param remote Remote used. + * @return Promise resolved when done. + */ + async updateTrackerGitInfo(branch, branchData, remote) { + // Get the repository data for the project. + let repositoryUrl = DevConfig.get(branchData.project + '.repositoryUrl'); + let diffUrlTemplate = DevConfig.get(branchData.project + '.diffUrlTemplate', ''); + + if (!repositoryUrl) { + // Calculate the repositoryUrl based on the remote URL. + repositoryUrl = await Git.getRemoteUrl(remote); + } + + // Make sure the repository URL uses the regular format. + repositoryUrl = repositoryUrl.replace(/^(git@|git:\/\/)/, 'https://') + .replace(/\.git$/, '') + .replace('github.com:', 'github.com/'); + + if (!diffUrlTemplate) { + diffUrlTemplate = Utils.concatenatePaths([repositoryUrl, 'compare/%headcommit%...%branch%']); + } + + // Now create the git URL for the repository. + const repositoryGitUrl = repositoryUrl.replace(/^https?:\/\//, 'git://') + '.git'; + + // Search HEAD commit to put in the diff URL. + console.log ('Searching for head commit...'); + let headCommit = await Git.getHeadCommit(branch, branchData); + + if (!headCommit) { + throw new Error('Head commit not resolved, aborting update of tracker fields'); + } + + headCommit = headCommit.substr(0, 10); + console.log(`Head commit resolved to ${headCommit}`); + + // Calculate last properties needed. + const diffUrl = diffUrlTemplate.replace('%branch%', branch).replace('%headcommit%', headCommit); + const fieldRepositoryUrl = DevConfig.get('tracker.fieldnames.repositoryurl', 'Pull from Repository'); + const fieldBranch = DevConfig.get('tracker.fieldnames.branch', 'Pull Master Branch'); + const fieldDiffUrl = DevConfig.get('tracker.fieldnames.diffurl', 'Pull Master Diff URL'); + + // Update tracker fields. + const updates = {}; + updates[fieldRepositoryUrl] = repositoryGitUrl; + updates[fieldBranch] = branch; + updates[fieldDiffUrl] = diffUrl; + + console.log('Setting tracker fields...'); + await Jira.setCustomFields(branchData.issue, updates); + } + + /** + * Validate commit messages comparing them with the branch name. + * + * @param branchData Parsed branch data. + * @return True if value is ok or the user wants to continue anyway, false to stop. + */ + async validateCommitMessages(branchData) { + const messages = await Git.messages(30); + + let numConsecutive = 0; + let wrongCommitCandidate = null; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + const issue = Utils.getIssueFromCommitMessage(message); + + if (!issue || issue != branchData.issue) { + if (i === 0) { + // Last commit is wrong, it shouldn't happen. Ask the user if he wants to continue. + if (!issue) { + console.log('The issue number could not be found in the last commit message.'); + console.log(`Commit: ${message}`); + } else if (issue != branchData.issue) { + console.log('The issue number in the last commit does not match the branch being pushed to.'); + console.log(`Branch: ${branchData.issue} vs. commit: ${issue}`); + } + + return this.askConfirmContinue(); + } + + numConsecutive++; + if (numConsecutive > 2) { + // 3 consecutive commits with different branch, probably the branch commits are over. Everything OK. + return true; + + // Don't treat a merge pull request commit as a wrong commit between right commits. + // The current push could be a quick fix after a merge. + } else if (!wrongCommitCandidate && message.indexOf('Merge pull request') == -1) { + wrongCommitCandidate = { + message: message, + issue: issue, + index: i, + }; + } + } else if (wrongCommitCandidate) { + // We've found a commit with the branch name after a commit with a different branch. Probably wrong commit. + if (!wrongCommitCandidate.issue) { + console.log('The issue number could not be found in one of the commit messages.'); + console.log(`Commit: ${wrongCommitCandidate.message}`); + } else { + console.log('The issue number in a certain commit does not match the branch being pushed to.'); + console.log(`Branch: ${branchData.issue} vs. commit: ${wrongCommitCandidate.issue}`); + console.log(`Commit message: ${wrongCommitCandidate.message}`); + } + + return this.askConfirmContinue(); + } + } + + return true; + } +} + +module.exports = PushTask; diff --git a/gulp/url.js b/gulp/url.js new file mode 100644 index 000000000..8b46409f6 --- /dev/null +++ b/gulp/url.js @@ -0,0 +1,79 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Class with helper functions for urls. + */ +class Url { + + /** + * Add params to a URL. + * + * @param url URL to add the params to. + * @param params Object with the params to add. + * @return URL with params. + */ + static addParamsToUrl(url, params) { + let separator = url.indexOf('?') != -1 ? '&' : '?'; + + for (const key in params) { + let value = params[key]; + + // Ignore objects. + if (typeof value != 'object') { + url += separator + key + '=' + value; + separator = '&'; + } + } + + return url; + } + + /** + * Parse parts of a url, using an implicit protocol if it is missing from the url. + * + * @param url Url. + * @return Url parts. + */ + static parse(url) { + // Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B. + const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); + + if (!match) { + return null; + } + + const host = match[4] || ''; + + // Get the credentials and the port from the host. + const [domainAndPort, credentials] = host.split('@').reverse(); + const [domain, port] = domainAndPort.split(':'); + const [username, password] = credentials ? credentials.split(':') : []; + + // Prepare parts replacing empty strings with undefined. + return { + protocol: match[2] || undefined, + domain: domain || undefined, + port: port || undefined, + credentials: credentials || undefined, + username: username || undefined, + password: password || undefined, + path: match[5] || undefined, + query: match[7] || undefined, + fragment: match[9] || undefined, + }; + } +} + +module.exports = Url; diff --git a/gulp/utils.js b/gulp/utils.js new file mode 100644 index 000000000..0ca11fa74 --- /dev/null +++ b/gulp/utils.js @@ -0,0 +1,119 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const DevConfig = require('./dev-config'); +const DEFAULT_ISSUE_REGEX = '^(MOBILE)[-_]([0-9]+)'; + +/** + * Class with some utility functions. + */ +class Utils { + /** + * Concatenate several paths, adding a slash between them if needed. + * + * @param paths List of paths. + * @return Concatenated path. + */ + static concatenatePaths(paths) { + if (!paths.length) { + return ''; + } + + // Remove all slashes between paths. + for (let i = 0; i < paths.length; i++) { + if (!paths[i]) { + continue; + } + + if (i === 0) { + paths[i] = String(paths[i]).replace(/\/+$/g, ''); + } else if (i === paths.length - 1) { + paths[i] = String(paths[i]).replace(/^\/+/g, ''); + } else { + paths[i] = String(paths[i]).replace(/^\/+|\/+$/g, ''); + } + } + + // Remove empty paths. + paths = paths.filter(path => !!path); + + return paths.join('/'); + } + + /** + * Get command line arguments. + * + * @return Object with command line arguments. + */ + static getCommandLineArguments() { + + let args = {}, opt, thisOpt, curOpt; + for (let a = 0; a < process.argv.length; a++) { + + thisOpt = process.argv[a].trim(); + opt = thisOpt.replace(/^\-+/, ''); + + if (opt === thisOpt) { + // argument value + if (curOpt) { + args[curOpt] = opt; + } + curOpt = null; + } + else { + // Argument name. + curOpt = opt; + args[curOpt] = true; + } + } + + return args; + } + + /** + * Given a commit message, return the issue name (e.g. MOBILE-1234). + * + * @param commit Commit message. + * @return Issue name. + */ + static getIssueFromCommitMessage(commit) { + const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); + const matches = commit.match(regex); + + return matches && matches[0]; + } + + /** + * Parse a branch name to extract some data. + * + * @param branch Branch name to parse. + * @return Data. + */ + static parseBranch(branch) { + const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); + + const matches = branch.match(regex); + if (!matches || matches.length < 3) { + throw new Error(`Error parsing branch ${branch}`); + } + + return { + issue: matches[0], + project: matches[1], + issueNumber: matches[2], + }; + } +} + +module.exports = Utils; diff --git a/gulpfile.js b/gulpfile.js index fbeead754..dcc2afde8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,437 +1,68 @@ -var gulp = require('gulp'), - fs = require('fs'), - through = require('through'), - rename = require('gulp-rename'), - path = require('path'), - slash = require('gulp-slash'), - clipEmptyFiles = require('gulp-clip-empty-files'), - File = require('vinyl'), - flatten = require('gulp-flatten'), - npmPath = require('path'), - concat = require('gulp-concat'), - htmlmin = require('gulp-htmlmin'), - bufferFrom = require('buffer-from'), - exec = require('child_process').exec, - license = '' + - '// (C) Copyright 2015 Moodle Pty Ltd.\n' + - '//\n' + - '// Licensed under the Apache License, Version 2.0 (the "License");\n' + - '// you may not use this file except in compliance with the License.\n' + - '// You may obtain a copy of the License at\n' + - '//\n' + - '// http://www.apache.org/licenses/LICENSE-2.0\n' + - '//\n' + - '// Unless required by applicable law or agreed to in writing, software\n' + - '// distributed under the License is distributed on an "AS IS" BASIS,\n' + - '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' + - '// See the License for the specific language governing permissions and\n' + - '// limitations under the License.\n\n'; +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -/** - * Copy a property from one object to another, adding a prefix to the key if needed. - * @param {Object} target Object to copy the properties to. - * @param {Object} source Object to copy the properties from. - * @param {String} prefix Prefix to add to the keys. - */ -function addProperties(target, source, prefix) { - for (var property in source) { - target[prefix + property] = source[property]; - } -} +const BuildConfigTask = require('./gulp/task-build-config'); +const BuildLangTask = require('./gulp/task-build-lang'); +const CombineScssTask = require('./gulp/task-combine-scss'); +const CopyComponentTemplatesTask = require('./gulp/task-copy-component-templates'); +const PushTask = require('./gulp/task-push'); +const Utils = require('./gulp/utils'); +const gulp = require('gulp'); +const pathLib = require('path'); -/** - * Treats a file to merge JSONs. This function is based on gulp-jsoncombine module. - * https://github.com/reflog/gulp-jsoncombine - * @param {Object} file File treated. - */ -function treatFile(file, data) { - if (file.isNull() || file.isStream()) { - return; // ignore - } - try { - var srcPos = file.path.lastIndexOf('/src/'); - if (srcPos == -1) { - // It's probably a Windows environment. - srcPos = file.path.lastIndexOf('\\src\\'); - } +const paths = { + lang: [ + './src/lang/', + './src/core/**/lang/', + './src/addon/**/lang/', + './src/assets/countries/', + './src/assets/mimetypes/' + ], + config: './src/config.json', +}; - var path = file.path.substr(srcPos + 5); - data[path] = JSON.parse(file.contents.toString()); - } catch (err) { - console.log('Error parsing JSON: ' + err); - } -} - -/** - * Treats the merged JSON data, adding prefixes depending on the component. Used in lang tasks. - * - * @param {Object} data Merged data. - * @return {Buffer} Buffer with the treated data. - */ -function treatMergedData(data) { - var merged = {}; - var mergedOrdered = {}; - - for (var filepath in data) { - var pathSplit = filepath.split(/[\/\\]/), - prefix; - - pathSplit.pop(); - - switch (pathSplit[0]) { - case 'lang': - prefix = 'core'; - break; - case 'core': - if (pathSplit[1] == 'lang') { - // Not used right now. - prefix = 'core'; - } else { - prefix = 'core.' + pathSplit[1]; - } - break; - case 'addon': - // Remove final item 'lang'. - pathSplit.pop(); - // Remove first item 'addon'. - pathSplit.shift(); - - // For subplugins. We'll use plugin_subfolder_subfolder2_... - // E.g. 'mod_assign_feedback_comments'. - prefix = 'addon.' + pathSplit.join('_'); - break; - case 'assets': - prefix = 'assets.' + pathSplit[1]; - break; - } - - if (prefix) { - addProperties(merged, data[filepath], prefix + '.'); - } - } - - // Force ordering by string key. - Object.keys(merged).sort().forEach(function(k){ - mergedOrdered[k] = merged[k]; - }); - - return bufferFrom(JSON.stringify(mergedOrdered, null, 4)); -} - -/** - * Build lang file. - * - * @param {String} language Language to translate. - * @param {String[]} langPaths Paths to the possible language files. - * @param {String} buildDest Path where to leave the built files. - * @param {Function} done Function to call when done. - * @return {Void} - */ -function buildLang(language, langPaths, buildDest, done) { - var filename = language + '.json', - data = {}, - firstFile = null; - - var paths = langPaths.map(function(path) { - if (path.slice(-1) != '/') { - path = path + '/'; - } - return path + language + '.json'; - }); - - gulp.src(paths, { allowEmpty: true }) - .pipe(slash()) - .pipe(clipEmptyFiles()) - .pipe(through(function(file) { - if (!firstFile) { - firstFile = file; - } - return treatFile(file, data); - }, function() { - /* This implementation is based on gulp-jsoncombine module. - * https://github.com/reflog/gulp-jsoncombine */ - if (firstFile) { - var joinedPath = path.join(firstFile.base, language+'.json'); - - var joinedFile = new File({ - cwd: firstFile.cwd, - base: firstFile.base, - path: joinedPath, - contents: treatMergedData(data) - }); - - this.emit('data', joinedFile); - } - this.emit('end'); - })) - .pipe(gulp.dest(buildDest)) - .on('end', done); -} - -// Delete a folder and all its contents. -function deleteFolderRecursive(path) { - if (fs.existsSync(path)) { - fs.readdirSync(path).forEach(function(file) { - var curPath = npmPath.join(path, file); - if (fs.lstatSync(curPath).isDirectory()) { - deleteFolderRecursive(curPath); - } else { - fs.unlinkSync(curPath); - } - }); - - fs.rmdirSync(path); - } -} - -// List of app lang files. To be used only if cannot get it from filesystem. -var paths = { - src: './src', - assets: './src/assets', - lang: [ - './src/lang/', - './src/core/**/lang/', - './src/addon/**/lang/', - './src/assets/countries/', - './src/assets/mimetypes/' - ], - config: './src/config.json', - }; +const args = Utils.getCommandLineArguments(); // Build the language files into a single file per language. -gulp.task('lang', function(done) { - buildLang('en', paths.lang, path.join(paths.assets, 'lang'), done); +gulp.task('lang', (done) => { + new BuildLangTask().run('en', paths.lang, done); }); // Convert config.json into a TypeScript class. -gulp.task('config', function(done) { - // Get the last commit. - exec('git log -1 --pretty=format:"%H"', function (err, commit, stderr) { - if (err) { - console.error('An error occurred while getting the last commit: ' + err); - } else if (stderr) { - console.error('An error occurred while getting the last commit: ' + stderr); - } +gulp.task('config', (done) => { + new BuildConfigTask().run(paths.config, done); +}); - gulp.src(paths.config) - .pipe(through(function(file) { - // Convert the contents of the file into a TypeScript class. - // Disable the rule variable-name in the file. - var config = JSON.parse(file.contents.toString()), - contents = license + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n', - that = this; +// Copy component templates to www to make compile-html work in AOT. +gulp.task('copy-component-templates', (done) => { + new CopyComponentTemplatesTask().run(done); +}); - for (var key in config) { - var value = config[key]; - if (typeof value == 'string') { - // Wrap the string in ' and scape them. - value = "'" + value.replace(/([^\\])'/g, "$1\\'") + "'"; - } else if (typeof value != 'number' && typeof value != 'boolean') { - // Stringify with 4 spaces of indentation, and then add 4 more spaces in each line. - value = JSON.stringify(value, null, 4).replace(/^(?: )/gm, ' ').replace(/^(?:})/gm, ' }'); - // Replace " by ' in values. - value = value.replace(/: "([^"]*)"/g, ": '$1'"); +// Combine SCSS files. +gulp.task('combine-scss', (done) => { + new CombineScssTask().run(done); +}); - // Check if the keys have "-" in it. - var matches = value.match(/"([^"]*\-[^"]*)":/g); - if (matches) { - // Replace " by ' in keys. We cannot remove them because keys have chars like '-'. - value = value.replace(/"([^"]*)":/g, "'$1':"); - } else { - // Remove ' in keys. - value = value.replace(/"([^"]*)":/g, "$1:"); - } - - // Add type any to the key. - key = key + ': any'; - } - - // If key has quotation marks, remove them. - if (key[0] == '"') { - key = key.substr(1, key.length - 2); - } - contents += ' static ' + key + ' = ' + value + ';\n'; - } - - // Add compilation info. - contents += ' static compilationtime = ' + Date.now() + ';\n'; - contents += ' static lastcommit = \'' + commit + '\';\n'; - - contents += '}\n'; - - file.contents = bufferFrom(contents); - this.emit('data', file); - })) - .pipe(rename('configconstants.ts')) - .pipe(gulp.dest(paths.src)) - .on('end', done); - }); +gulp.task('push', (done) => { + new PushTask().run(args, done); }); gulp.task('default', gulp.parallel('lang', 'config')); -gulp.task('watch', function() { - var langsPaths = paths.lang.map(function(path) { - return path + 'en.json'; - }); +gulp.task('watch', () => { + const langsPaths = paths.lang.map(path => path + 'en.json'); + gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang')); gulp.watch(paths.config, { interval: 500 }, gulp.parallel('config')); }); - -var templatesSrc = [ - './src/components/**/*.html', - './src/core/**/components/**/*.html', - './src/core/**/component/**/*.html', - // Copy all addon components because any component can be injected using extraImports. - './src/addon/**/components/**/*.html', - './src/addon/**/component/**/*.html' - ], - templatesDest = './www/templates'; - -// Copy component templates to www to make compile-html work in AOT. -gulp.task('copy-component-templates', function(done) { - deleteFolderRecursive(templatesDest); - - gulp.src(templatesSrc, { allowEmpty: true }) - .pipe(flatten()) - // Check options here: https://github.com/kangax/html-minifier - .pipe(htmlmin({ - collapseWhitespace: true, - removeComments: true, - caseSensitive: true - })) - .pipe(gulp.dest(templatesDest)) - .on('end', done); -}); - -/** - * Finds the file and returns its content. - * - * @param {string} capture Import file path. - * @param {string} baseDir Directory where the file was found. - * @param {string} paths Alternative paths where to find the imports. - * @param {Array} parsedFiles Yet parsed files to reduce size of the result. - * @return {string} Partially combined scss. - */ -function getReplace(capture, baseDir, paths, parsedFiles) { - var parse = path.parse(path.resolve(baseDir, capture + '.scss')); - var file = parse.dir + '/' + parse.name; - - if (file.slice(-3) === '.wp') { - console.log('Windows Phone not supported "' + capture); - // File was already parsed, leave the import commented. - return '// @import "' + capture + '";'; - } - - if (!fs.existsSync(file + '.scss')) { - // File not found, might be a partial file. - file = parse.dir + '/_' + parse.name; - } - - // If file still not found, try to find the file in the alternative paths. - var x = 0; - while (!fs.existsSync(file + '.scss') && paths.length > x) { - parse = path.parse(path.resolve(paths[x], capture + '.scss')); - file = parse.dir + '/' + parse.name; - - x++; - } - - file = file + '.scss'; - - if (!fs.existsSync(file)) { - // File not found. Leave the import there. - console.log('File "' + capture + '" not found'); - return '@import "' + capture + '";'; - } - - if (parsedFiles.indexOf(file) >= 0) { - console.log('File "' + capture + '" already parsed'); - // File was already parsed, leave the import commented. - return '// @import "' + capture + '";'; - } - - parsedFiles.push(file); - var text = fs.readFileSync(file); - - // Recursive call. - return scssCombine(text, parse.dir, paths, parsedFiles); -} - -/** - * Combine scss files with its imports - * - * @param {string} content Scss string to read. - * @param {string} baseDir Directory where the file was found. - * @param {string} paths Alternative paths where to find the imports. - * @param {Array} parsedFiles Yet parsed files to reduce size of the result. - * @return {string} Scss string with the replaces done. - */ -function scssCombine(content, baseDir, paths, parsedFiles) { - - // Content is a Buffer, convert to string. - if (typeof content != "string") { - content = content.toString(); - } - - // Search of single imports. - var regex = /@import[ ]*['"](.*)['"][ ]*;/g; - - if (regex.test(content)) { - return content.replace(regex, function(m, capture) { - if (capture == "bmma") { - return m; - } - - return getReplace(capture, baseDir, paths, parsedFiles); - }); - } - - // Search of multiple imports. - regex = /@import(?:[ \n]+['"](.*)['"][,]?[ \n]*)+;/gm; - if (regex.test(content)) { - return content.replace(regex, function(m, capture) { - var text = ""; - - // Divide the import into multiple files. - regex = /['"]([^'"]*)['"]/g; - var captures = m.match(regex); - for (var x in captures) { - text += getReplace(captures[x].replace(/['"]+/g, ''), baseDir, paths, parsedFiles) + "\n"; - } - - return text; - }); - } - - return content; -} - -gulp.task('combine-scss', function(done) { - var paths = [ - 'node_modules/ionic-angular/themes/', - 'node_modules/font-awesome/scss/', - 'node_modules/ionicons/dist/scss/' - ]; - - var parsedFiles = []; - - gulp.src([ - './src/theme/variables.scss', - './node_modules/ionic-angular/themes/ionic.globals.*.scss', - './node_modules/ionic-angular/themes/ionic.components.scss', - './src/**/*.scss']) // define a source files - .pipe(through(function(file, encoding, callback) { - if (file.isNull()) { - return; - } - - parsedFiles.push(file); - file.contents = bufferFrom(scssCombine(file.contents, path.dirname(file.path), paths, parsedFiles)); - - this.emit('data', file); - })) // combine them based on @import and save it to stream - .pipe(concat('combined.scss')) // concat the stream output in single file - .pipe(gulp.dest('.')) // save file to destination. - .on('end', done); -}); diff --git a/package-lock.json b/package-lock.json index 4075a47e6..8f92002a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.9.2", + "version": "3.9.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -127,6 +127,11 @@ "resolved": "https://registry.npmjs.org/@ionic-native/device/-/device-4.20.0.tgz", "integrity": "sha512-ogHZwlC1GLbj2sL/eRp+RDs7bWc1AuwKNhgtDLE3yjXey09I5ErkADLydugMTEYoU/Wja9+YjXdZGymuaHwgNg==" }, + "@ionic-native/diagnostic": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ionic-native/diagnostic/-/diagnostic-4.2.0.tgz", + "integrity": "sha512-hCRYVseQrsbuA4EKgvmwJB/nweHcrBK+avE2+GyYcUFoNNhM5yz9i7mlw4J0Vw5mr2udAVhPdLyvJV5p1AkU4g==" + }, "@ionic-native/file": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@ionic-native/file/-/file-4.20.0.tgz", @@ -172,6 +177,11 @@ "resolved": "https://registry.npmjs.org/@ionic-native/local-notifications/-/local-notifications-4.20.0.tgz", "integrity": "sha512-Ht/0zau8/2+G/bH/okXXhhWB6YrkCNL2QxVJHQ2dophXFGxQPOZAN3CKWhuQSjfbr76fa2nvQXF6jsXLpIR/ng==" }, + "@ionic-native/media": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@ionic-native/media/-/media-4.20.0.tgz", + "integrity": "sha512-uhuTvy7MT6zFMSTDX/0aIrGu8IeRGi2FWJbWE+6o5wttAeVA6hNISSbtj4OQZhL3sUXYNCczDayV1VsOcXbdUg==" + }, "@ionic-native/media-capture": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@ionic-native/media-capture/-/media-capture-4.20.0.tgz", @@ -634,20 +644,21 @@ } }, "@ionic/cli": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/@ionic/cli/-/cli-6.9.3.tgz", - "integrity": "sha512-pTMSFczhjMpPnh8fnxuMGU4tcvPlUYZPambNKZdFzRVNQasK00kqrR/Vc8dlHNNRjB/99Hu+wu3H68/7ooU6ww==", + "version": "6.11.8", + "resolved": "https://registry.npmjs.org/@ionic/cli/-/cli-6.11.8.tgz", + "integrity": "sha512-AccCEMRLxzBBhpN/id6DvTAhU5NkNo/tSbuTTIbEgEZgyH6n+TlN9e7iFzmOhLKkwwwwDy7H4iYSR+xT38YaFw==", "dev": true, "requires": { - "@ionic/cli-framework": "4.1.5", - "@ionic/cli-framework-prompts": "2.1.3", - "@ionic/utils-array": "2.1.3", - "@ionic/utils-fs": "3.1.3", - "@ionic/utils-network": "2.1.3", - "@ionic/utils-process": "2.1.3", - "@ionic/utils-stream": "3.1.3", - "@ionic/utils-subprocess": "2.1.3", - "@ionic/utils-terminal": "2.1.3", + "@ionic/cli-framework": "5.0.4", + "@ionic/cli-framework-output": "2.2.0", + "@ionic/cli-framework-prompts": "2.1.6", + "@ionic/utils-array": "2.1.5", + "@ionic/utils-fs": "3.1.5", + "@ionic/utils-network": "2.1.5", + "@ionic/utils-process": "2.1.6", + "@ionic/utils-stream": "3.1.5", + "@ionic/utils-subprocess": "2.1.6", + "@ionic/utils-terminal": "2.2.1", "chalk": "^4.0.0", "debug": "^4.0.0", "diff": "^4.0.1", @@ -655,7 +666,7 @@ "leek": "0.0.24", "lodash": "^4.17.5", "open": "^7.0.4", - "os-name": "^3.1.0", + "os-name": "^4.0.0", "semver": "^7.1.1", "split2": "^3.0.0", "ssh-config": "^1.1.1", @@ -663,11 +674,20 @@ "superagent": "^5.2.1", "superagent-proxy": "^2.0.0", "tar": "^6.0.1", - "through2": "^3.0.0", - "tslib": "1.11.2", - "uuid": "^7.0.1" + "tslib": "^2.0.1" }, "dependencies": { + "@ionic/utils-fs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.5.tgz", + "integrity": "sha512-a41bY2dHqWSEQQ/80CpbXSs8McyiCFf2DnIWWLukrhYWf46h4qi6M/8dxcMKrofRiqI/3F+cL3S2mOm9Zz/o2Q==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -679,9 +699,9 @@ } }, "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -709,6 +729,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -724,6 +755,35 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "execa": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -733,16 +793,53 @@ "minipass": "^3.0.0" } }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, "macos-release": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", - "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz", + "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, "minipass": { @@ -755,9 +852,9 @@ } }, "minizlib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", - "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "requires": { "minipass": "^3.0.0", @@ -776,14 +873,48 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-4.0.0.tgz", + "integrity": "sha512-caABzDdJMbtykt7GmSogEat3faTKQhmZf0BS5l/pZGmP0vPWQjXWqOhbLyK+b6j2/DQPmEvYdzLXJXXLJNVDNg==", "dev": true, "requires": { "macos-release": "^2.2.0", - "windows-release": "^3.1.0" + "windows-release": "^4.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "semver": { @@ -792,49 +923,73 @@ "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } }, "tar": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz", - "integrity": "sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", "dev": true, "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^3.0.0", - "minizlib": "^2.1.0", + "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { - "readable-stream": "2 || 3" + "isexe": "^2.0.0" } }, - "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", - "dev": true - }, - "uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", - "dev": true + "windows-release": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", + "integrity": "sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==", + "dev": true, + "requires": { + "execa": "^4.0.2" + } }, "yallist": { "version": "4.0.0", @@ -845,38 +1000,39 @@ } }, "@ionic/cli-framework": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@ionic/cli-framework/-/cli-framework-4.1.5.tgz", - "integrity": "sha512-rrFJQ4hyQYPsl//sF8zLzMwSstHj/OjY3Ac8gTJ7jDETDYus5wfOr4EAEUcnpvaCePPcSI6bTvh1Bpkr04Ng9A==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework/-/cli-framework-5.0.4.tgz", + "integrity": "sha512-8sO+OjvKxW/ofBngRzydWRq1ATDwHPgQ6occTVEBcSNI9wCiKfLA9GKuKRYfmVvCDX84/g3BAWr6IdErRqODHw==", "dev": true, "requires": { - "@ionic/utils-array": "2.1.3", - "@ionic/utils-fs": "3.1.3", - "@ionic/utils-object": "2.1.3", - "@ionic/utils-process": "2.1.3", - "@ionic/utils-stream": "3.1.3", - "@ionic/utils-subprocess": "2.1.3", - "@ionic/utils-terminal": "2.1.3", + "@ionic/cli-framework-output": "2.2.0", + "@ionic/utils-array": "2.1.5", + "@ionic/utils-fs": "3.1.5", + "@ionic/utils-object": "2.1.5", + "@ionic/utils-process": "2.1.6", + "@ionic/utils-stream": "3.1.5", + "@ionic/utils-subprocess": "2.1.6", + "@ionic/utils-terminal": "2.2.1", "chalk": "^4.0.0", "debug": "^4.0.0", "lodash": "^4.17.5", - "log-update": "^4.0.0", "minimist": "^1.2.0", "rimraf": "^3.0.0", - "slice-ansi": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "tslib": "1.11.2", + "tslib": "^2.0.1", "untildify": "^4.0.0", - "wrap-ansi": "^7.0.0", "write-file-atomic": "^3.0.0" }, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "@ionic/utils-fs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.5.tgz", + "integrity": "sha512-a41bY2dHqWSEQQ/80CpbXSs8McyiCFf2DnIWWLukrhYWf46h4qi6M/8dxcMKrofRiqI/3F+cL3S2mOm9Zz/o2Q==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + } }, "ansi-styles": { "version": "4.2.1", @@ -889,9 +1045,9 @@ } }, "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -922,11 +1078,17 @@ "ms": "^2.1.1" } }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } }, "glob": { "version": "7.1.6", @@ -942,17 +1104,27 @@ "path-is-absolute": "^1.0.0" } }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } }, "ms": { "version": "2.1.2", @@ -969,87 +1141,45 @@ "glob": "^7.1.3" } }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", "dev": true }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true } } }, - "@ionic/cli-framework-prompts": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@ionic/cli-framework-prompts/-/cli-framework-prompts-2.1.3.tgz", - "integrity": "sha512-tYjXIkoUdl6SPDuLHsUXbIyXLiwofsqNjkrNhbJ2Ed8oSiBlhfio/XZ1nKAEHaoHpud+UfD5t++goYnGVa4fBw==", + "@ionic/cli-framework-output": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.0.tgz", + "integrity": "sha512-WIWLTlkG8mGOyLPxwmR1Ybwtb0FnAKfw0tl77S4Br6xj/WlKNDD0rgMZfgcn7sMOF98IqkeQgDM9rvPJrgiq4Q==", "dev": true, "requires": { - "@ionic/utils-terminal": "2.1.3", + "@ionic/utils-terminal": "2.2.1", "debug": "^4.0.0", - "inquirer": "^7.0.0", - "tslib": "1.11.2" + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "wrap-ansi": "^7.0.0" }, "dependencies": { - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - } - }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -1066,25 +1196,6 @@ "color-convert": "^2.0.1" } }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1115,94 +1226,24 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "inquirer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", - "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.15", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.5.3", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - } - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, - "rxjs": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", - "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -1223,31 +1264,35 @@ "ansi-regex": "^5.0.0" } }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } - }, - "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", - "dev": true } } }, - "@ionic/utils-array": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.3.tgz", - "integrity": "sha512-IV7oK7kj6UZEkZ5lbS78gNSUSTqZtLOEKu9G+MqBpRTX+YKKnmsAxQuvZrnsy/pHmzJ7aKlj1V0gNAFO6w/NOA==", + "@ionic/cli-framework-prompts": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-prompts/-/cli-framework-prompts-2.1.6.tgz", + "integrity": "sha512-hKyXyBlOA4KBHGKSIX1uHiaviZ/V7cGTlB3ZmmfXu7xqEmSuT8NXwP3VTazIm0tXuVXMXlgKEewikaH6P9VrVA==", "dev": true, "requires": { + "@ionic/utils-terminal": "2.2.1", "debug": "^4.0.0", - "tslib": "1.11.2" + "inquirer": "^7.0.0", + "tslib": "^2.0.1" }, "dependencies": { "debug": { @@ -1266,9 +1311,42 @@ "dev": true }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + } + } + }, + "@ionic/utils-array": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.5.tgz", + "integrity": "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", "dev": true } } @@ -1352,13 +1430,13 @@ } }, "@ionic/utils-network": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@ionic/utils-network/-/utils-network-2.1.3.tgz", - "integrity": "sha512-6R40gzy8vr2CTV/gvq4uTSZrkviR1IEtT1M4T+9KnTO6+tFLg08oilpJrxPvZh4SMr+VdIIus+LSOtdBzI+Dwg==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-network/-/utils-network-2.1.5.tgz", + "integrity": "sha512-HUQ1Ec4Mh2MXzzKdbbbDS6xYKwpFJ2XRY7SYXbaZT8+jiNahfHbsOfe62/p8bk41Yil7E9EagzGC2JvIFJh01w==", "dev": true, "requires": { "debug": "^4.0.0", - "tslib": "1.11.2" + "tslib": "^2.0.1" }, "dependencies": { "debug": { @@ -1377,21 +1455,21 @@ "dev": true }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", "dev": true } } }, "@ionic/utils-object": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.3.tgz", - "integrity": "sha512-iJQjC2RBWACCgwafsKKJN+G2hxTxRhVT0gtdGK29jH8ZZMIJGEEIA2hlpPsL9OR/yRMwByATyO1usC2jEM8qDQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", + "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", "dev": true, "requires": { "debug": "^4.0.0", - "tslib": "1.11.2" + "tslib": "^2.0.1" }, "dependencies": { "debug": { @@ -1410,25 +1488,25 @@ "dev": true }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", "dev": true } } }, "@ionic/utils-process": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.3.tgz", - "integrity": "sha512-FMW9kc+waKv01/dNuMOP3NrJLyhMv8Ij73B2KVlZyI6UiFMjghvEApttQVi2ewKw6z1ipbkSFRaICxPIvGwABw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.6.tgz", + "integrity": "sha512-HuMTJp6lkzMZAI+FUEqLoOx6wxd29ysmk0Q9dCHABbIEP4hM5zAXTIuv4qUYelzT6lBywS/TAdfz+XB1TM6umg==", "dev": true, "requires": { - "@ionic/utils-object": "2.1.3", - "@ionic/utils-terminal": "2.1.3", + "@ionic/utils-object": "2.1.5", + "@ionic/utils-terminal": "2.2.1", "debug": "^4.0.0", - "lodash": "^4.17.5", + "signal-exit": "^3.0.3", "tree-kill": "^1.2.2", - "tslib": "1.11.2" + "tslib": "^2.0.1" }, "dependencies": { "debug": { @@ -1446,22 +1524,28 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", "dev": true } } }, "@ionic/utils-stream": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.3.tgz", - "integrity": "sha512-vhYVJMT/5HNhTe3ypPkgll4e4O6fRXqAEXntJzBy3COWllXkERB/tWn2x8TSLcosmzN8f8FONCjznnq6Uq54LQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.5.tgz", + "integrity": "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==", "dev": true, "requires": { "debug": "^4.0.0", - "tslib": "1.11.2" + "tslib": "^2.0.1" }, "dependencies": { "debug": { @@ -1480,29 +1564,40 @@ "dev": true }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", "dev": true } } }, "@ionic/utils-subprocess": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.3.tgz", - "integrity": "sha512-jTEz97eFxNLj+Cw8CQvfkNThEBT98wrbPfE2C3MO9E+gw+h7gX2/KWoUgg9U0lAyiOUggCVir/44SLbevhtPBw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.6.tgz", + "integrity": "sha512-osM2CaD+bxf4WXfQEjkPXHPqzg4xUqg7Rm9FK/DKddIuL6fHPoeLOwP7t0L3J4417dYsvtssTjxbSQnm/V5aWw==", "dev": true, "requires": { - "@ionic/utils-array": "2.1.3", - "@ionic/utils-fs": "3.1.3", - "@ionic/utils-process": "2.1.3", - "@ionic/utils-stream": "3.1.3", - "@ionic/utils-terminal": "2.1.3", + "@ionic/utils-array": "2.1.5", + "@ionic/utils-fs": "3.1.5", + "@ionic/utils-process": "2.1.6", + "@ionic/utils-stream": "3.1.5", + "@ionic/utils-terminal": "2.2.1", "cross-spawn": "^7.0.0", "debug": "^4.0.0", - "tslib": "1.11.2" + "tslib": "^2.0.1" }, "dependencies": { + "@ionic/utils-fs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.5.tgz", + "integrity": "sha512-a41bY2dHqWSEQQ/80CpbXSs8McyiCFf2DnIWWLukrhYWf46h4qi6M/8dxcMKrofRiqI/3F+cL3S2mOm9Zz/o2Q==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1523,6 +1618,34 @@ "ms": "^2.1.1" } }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1551,9 +1674,15 @@ "dev": true }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", "dev": true }, "which": { @@ -1568,13 +1697,13 @@ } }, "@ionic/utils-terminal": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.1.3.tgz", - "integrity": "sha512-By0tp8pBcghIqTMKnRQw55cnUhDGrE+UTfdY81iDiXaMxRy8vwJjXgq9jiCKLF/qH1elQpIzDo2ePdu+MrNMFg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.2.1.tgz", + "integrity": "sha512-YBXSpK5rHu3XlQCF01S23Y4Rz1msBRqNBgGDo8lXekeRmI2WgeCxMHFZfKTEh30DQNYibnkwaeLacHp6ohd+8g==", "dev": true, "requires": { "debug": "^4.0.0", - "tslib": "1.11.2" + "tslib": "^2.0.1" }, "dependencies": { "debug": { @@ -1593,20 +1722,28 @@ "dev": true }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", "dev": true } } }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "@netflix/nerror": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@netflix/nerror/-/nerror-1.1.3.tgz", + "integrity": "sha512-b+MGNyP9/LXkapreJzNUzcvuzZslj/RGgdVVJ16P2wSlYatfLycPObImqVJSmNAdyeShvNeM/pl3sVZsObFueg==", "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" + "assert-plus": "^1.0.0", + "extsprintf": "^1.4.0", + "lodash": "^4.17.15" + }, + "dependencies": { + "extsprintf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz", + "integrity": "sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=" + } } }, "@ngx-translate/core": { @@ -1619,16 +1756,46 @@ "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-2.0.1.tgz", "integrity": "sha1-qmd4jmS/qGUmkad7Ais7QDEgkRM=" }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==" + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/cordova": { "version": "0.0.34", @@ -1666,30 +1833,11 @@ "integrity": "sha1-+iycaufkxX8Tt39pXaTtuzr6oBY=", "dev": true }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" - }, "@types/node": { "version": "8.10.59", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", - "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" + "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==", + "dev": true }, "@types/promise.prototype.finally": { "version": "2.0.4", @@ -1809,17 +1957,17 @@ "integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=" }, "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", "requires": { - "string-width": "^2.0.0" + "string-width": "^3.0.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "is-fullwidth-code-point": { "version": "2.0.0", @@ -1827,20 +1975,21 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { + "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "strip-ansi": "^5.1.0" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } @@ -1864,9 +2013,12 @@ } }, "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "requires": { + "type-fest": "^0.11.0" + } }, "ansi-gray": { "version": "0.1.1", @@ -1889,8 +2041,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -1942,8 +2093,7 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "archy": { "version": "1.0.0", @@ -1955,7 +2105,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -2004,7 +2153,8 @@ "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true }, "array-each": { "version": "1.0.1", @@ -2088,17 +2238,9 @@ } }, "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" }, "array-unique": { "version": "0.2.1", @@ -2155,13 +2297,25 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true }, "ast-types": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", - "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==", - "dev": true + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.1.tgz", + "integrity": "sha512-pfSiukbt23P1qMhNnsozLzhMLBs7EEeXqPyvPmnuZM+RMfwfqwDbSVKYflgGuVI7/VehR4oMks0igzdNAg4VeQ==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + } + } }, "astral-regex": { "version": "2.0.0", @@ -2229,13 +2383,13 @@ "at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" }, "atob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true }, "autoprefixer": { "version": "7.2.6", @@ -2257,9 +2411,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" }, "babel-code-frame": { "version": "6.26.0", @@ -2330,6 +2484,7 @@ "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, "requires": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", @@ -2344,6 +2499,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -2352,6 +2508,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2360,6 +2517,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2368,6 +2526,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -2377,12 +2536,14 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true } } }, @@ -2425,6 +2586,46 @@ "file-uri-to-path": "1.0.0" } }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "optional": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "optional": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "optional": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -2495,6 +2696,7 @@ "version": "1.18.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "dev": true, "requires": { "bytes": "3.0.0", "content-type": "~1.0.4", @@ -2509,50 +2711,106 @@ } }, "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } }, "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.0" } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" } } }, @@ -2833,6 +3091,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, "requires": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", @@ -2848,14 +3107,48 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } }, "callsites": { "version": "3.1.0", @@ -2894,11 +3187,6 @@ "integrity": "sha512-ekW8NQ3/FvokviDxhdKLZZAx7PptXNwxKgXtnR5y+PR3hckwuP3yJ1Ir+4/c97dsHNqtAyfKUGdw8P4EYzBNgw==", "dev": true }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==" - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -2918,6 +3206,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -2974,8 +3263,7 @@ "chownr": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", - "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", - "dev": true + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==" }, "chromium-pickle-js": { "version": "0.2.0", @@ -2986,7 +3274,8 @@ "ci-info": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", - "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==" + "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", + "dev": true }, "cipher-base": { "version": "1.0.4", @@ -3002,6 +3291,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, "requires": { "arr-union": "^3.1.0", "define-property": "^0.2.5", @@ -3013,6 +3303,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -3020,7 +3311,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -3034,9 +3326,9 @@ } }, "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" }, "cli-cursor": { "version": "2.1.0", @@ -3047,9 +3339,9 @@ } }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==" }, "cliui": { "version": "3.2.0", @@ -3074,6 +3366,21 @@ "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", "dev": true }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + }, + "dependencies": { + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + } + } + }, "clone-stats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", @@ -3100,8 +3407,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collection-map": { "version": "1.0.0", @@ -3129,6 +3435,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, "requires": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" @@ -3150,6 +3457,12 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, "com-darryncampbell-cordova-plugin-intent": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/com-darryncampbell-cordova-plugin-intent/-/com-darryncampbell-cordova-plugin-intent-1.3.0.tgz", @@ -3170,9 +3483,9 @@ "dev": true }, "compare-func": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", - "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.4.tgz", + "integrity": "sha512-sq2sWtrqKPkEXAC8tEJA1+BqAH9GbFkGBtUOqrUX57VSfwp8xyktctk+uLoRy5eccTdxzDcVIztlYDpKs3Jv1Q==", "requires": { "array-ify": "^1.0.0", "dot-prop": "^3.0.0" @@ -3185,6 +3498,11 @@ "requires": { "is-obj": "^1.0.0" } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" } } }, @@ -3197,20 +3515,21 @@ "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true }, "compressible": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", - "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "requires": { - "mime-db": ">= 1.40.0 < 2" + "mime-db": ">= 1.43.0 < 2" }, "dependencies": { "mime-db": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", - "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" } } }, @@ -3264,19 +3583,57 @@ "make-dir": "^1.0.0", "pkg-up": "^2.0.0", "write-file-atomic": "^2.3.0" + }, + "dependencies": { + "dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "requires": { + "is-obj": "^1.0.0" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + } } }, "configstore": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-4.0.0.tgz", - "integrity": "sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", "requires": { - "dot-prop": "^4.1.0", + "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" } }, "console-browserify": { @@ -3291,8 +3648,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "constants-browserify": { "version": "1.0.0", @@ -3303,7 +3659,8 @@ "content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true }, "content-type": { "version": "1.0.4", @@ -3328,7 +3685,8 @@ "cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true }, "cookie-signature": { "version": "1.0.6", @@ -3344,7 +3702,8 @@ "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true }, "copy-props": { "version": "2.0.4", @@ -3357,20 +3716,141 @@ } }, "cordova": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cordova/-/cordova-9.0.0.tgz", - "integrity": "sha512-zWEPo9uGj9KNcEhU2Lpo3r4HYK21tL+at496N2LLnuCWuWVndv6QWed8+EYl/08rrcNshrEtfzXj9Ux6vQm2PQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/cordova/-/cordova-10.0.0.tgz", + "integrity": "sha512-00wMcj3X9ILhKtvRG2iEwO2qly4B+vgXFhH4WhVepWg2UVbD1opl1q9jSZ+j2AaI/vsBWW8e6M2M5FAHasnuWw==", "requires": { - "configstore": "^4.0.0", - "cordova-common": "^3.1.0", - "cordova-lib": "^9.0.0", + "configstore": "^5.0.1", + "cordova-common": "^4.0.2", + "cordova-create": "^3.0.0", + "cordova-lib": "^10.0.0", "editor": "^1.0.0", - "insight": "^0.10.1", - "loud-rejection": "^2.0.0", - "nopt": "^4.0.1", - "update-notifier": "^2.5.0" + "execa": "^4.0.3", + "fs-extra": "^9.0.1", + "insight": "^0.10.3", + "loud-rejection": "^2.2.0", + "nopt": "^4.0.3", + "semver": "^7.3.2", + "systeminformation": "^4.26.10", + "update-notifier": "^4.1.0" }, "dependencies": { + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "requires": { + "big-integer": "^1.6.44" + } + }, + "cordova-common": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cordova-common/-/cordova-common-4.0.2.tgz", + "integrity": "sha512-od7aNShyuBajzPY83mUEO8tERwwWdFklXETHiXP5Ft87CWeo/tSuwNPFztyTy8XYc74yXdogXKPTJeUHuVzB8Q==", + "requires": { + "@netflix/nerror": "^1.1.3", + "ansi": "^0.3.1", + "bplist-parser": "^0.2.0", + "cross-spawn": "^7.0.1", + "elementtree": "^0.1.7", + "endent": "^1.4.1", + "fast-glob": "^3.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "plist": "^3.0.1", + "q": "^1.5.1", + "read-chunk": "^3.2.0", + "strip-bom": "^4.0.0", + "underscore": "^1.9.2" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "endent": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/endent/-/endent-1.4.1.tgz", + "integrity": "sha512-buHTb5c8AC9NshtP6dgmNLYkiT+olskbq1z6cEGvfGCF3Qphbu/1zz5Xu+yjTDln8RbxNhPoUyJ5H8MSrp1olQ==", + "requires": { + "dedent": "^0.7.0", + "fast-json-parse": "^1.0.3", + "objectorarray": "^1.0.4" + } + }, + "execa": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, "loud-rejection": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-2.2.0.tgz", @@ -3380,14 +3860,95 @@ "signal-exit": "^3.0.2" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "requires": { "abbrev": "1", "osenv": "^0.1.4" } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "objectorarray": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/objectorarray/-/objectorarray-1.0.4.tgz", + "integrity": "sha512-91k8bjcldstRz1bG6zJo8lWD7c6QXcB4nTDUqiEvIL1xAsLoZlOOZZG+nd6YPz+V7zY1580J4Xxh1vZtyv4i/w==" + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "underscore": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", + "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } } } }, @@ -3433,9 +3994,9 @@ } }, "cordova-app-hello-world": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cordova-app-hello-world/-/cordova-app-hello-world-4.0.0.tgz", - "integrity": "sha512-hTNYHUJT5YyMa1cQQE1naGyU6Eh5D5Jl33sMnCh3+q15ZwWTL/TOy3k8+mUvjTp8bwhO5eECGKULYoVO+fp9ZA==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cordova-app-hello-world/-/cordova-app-hello-world-5.0.0.tgz", + "integrity": "sha512-5My01wsYoeYwS0f/t5Ck52xPm0+2zYJ0SlvxG9vUsndDGtgiP6t/G8upPgWcyDRRz7Rs/50yZuOntmHqmJxccQ==" }, "cordova-clipboard": { "version": "1.3.0", @@ -3497,85 +4058,348 @@ } }, "cordova-create": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cordova-create/-/cordova-create-2.0.0.tgz", - "integrity": "sha512-72CaGg/7x+tiZlzeXKQXLTc8Jh4tbwLdu4Ib97kJ6+R3bcew/Yv/l2cVA2E0CaCuOCtouTqwi+YLcA2I4dPFTQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cordova-create/-/cordova-create-3.0.0.tgz", + "integrity": "sha512-WxZRTnt5RHxSAB9urnHFUtVBcIe1YjR4sfwHLsxakNoKkFhcie3HrV5QmNBgRQ5DkxmanRN3VSx4OrPVsNmAaQ==", "requires": { - "cordova-app-hello-world": "^4.0.0", - "cordova-common": "^3.1.0", - "cordova-fetch": "^2.0.0", - "fs-extra": "^7.0.1", - "import-fresh": "^3.0.0", - "is-url": "^1.2.4", - "isobject": "^3.0.1", + "cordova-app-hello-world": "^5.0.0", + "cordova-common": "^4.0.1", + "cordova-fetch": "^3.0.0", + "fs-extra": "^9.0.0", + "globby": "^11.0.0", + "import-fresh": "^3.2.1", + "isobject": "^4.0.0", + "npm-package-arg": "^8.0.1", "path-is-inside": "^1.0.2", - "tmp": "0.0.33", + "tmp": "^0.2.1", "valid-identifier": "0.0.2" }, "dependencies": { - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "big-integer": "^1.6.44" } }, + "cordova-common": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cordova-common/-/cordova-common-4.0.2.tgz", + "integrity": "sha512-od7aNShyuBajzPY83mUEO8tERwwWdFklXETHiXP5Ft87CWeo/tSuwNPFztyTy8XYc74yXdogXKPTJeUHuVzB8Q==", + "requires": { + "@netflix/nerror": "^1.1.3", + "ansi": "^0.3.1", + "bplist-parser": "^0.2.0", + "cross-spawn": "^7.0.1", + "elementtree": "^0.1.7", + "endent": "^1.4.1", + "fast-glob": "^3.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "plist": "^3.0.1", + "q": "^1.5.1", + "read-chunk": "^3.2.0", + "strip-bom": "^4.0.0", + "underscore": "^1.9.2" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "endent": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/endent/-/endent-1.4.1.tgz", + "integrity": "sha512-buHTb5c8AC9NshtP6dgmNLYkiT+olskbq1z6cEGvfGCF3Qphbu/1zz5Xu+yjTDln8RbxNhPoUyJ5H8MSrp1olQ==", + "requires": { + "dedent": "^0.7.0", + "fast-json-parse": "^1.0.3", + "objectorarray": "^1.0.4" + } + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", + "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==" + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "objectorarray": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/objectorarray/-/objectorarray-1.0.4.tgz", + "integrity": "sha512-91k8bjcldstRz1bG6zJo8lWD7c6QXcB4nTDUqiEvIL1xAsLoZlOOZZG+nd6YPz+V7zY1580J4Xxh1vZtyv4i/w==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, + "underscore": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", + "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } } } }, "cordova-fetch": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cordova-fetch/-/cordova-fetch-2.0.1.tgz", - "integrity": "sha512-q21PeobERzE3Drli5htcl5X9Mtfvodih5VkqIwdRUsjDBCPv+I6ZonRjYGbNnXhYrYx7dm0m0j/7/Smf6Av3hg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cordova-fetch/-/cordova-fetch-3.0.0.tgz", + "integrity": "sha512-N6mB/1GD8BNclxnfO85E4/s46nEJjIxYeJYHRGi6MjofhigJ3NlGwTCslbTcq8IOYEh0RdoA0mS4W2jA5UcWeQ==", "requires": { - "cordova-common": "^3.1.0", - "fs-extra": "^7.0.1", - "npm-package-arg": "^6.1.0", - "pify": "^4.0.1", - "resolve": "^1.10.0", - "semver": "^5.6.0", - "which": "^1.3.1" + "cordova-common": "^4.0.0", + "fs-extra": "^9.0.0", + "npm-package-arg": "^8.0.1", + "pify": "^5.0.0", + "resolve": "^1.15.1", + "semver": "^7.1.3", + "which": "^2.0.2" }, "dependencies": { - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "big-integer": "^1.6.44" } }, + "cordova-common": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cordova-common/-/cordova-common-4.0.2.tgz", + "integrity": "sha512-od7aNShyuBajzPY83mUEO8tERwwWdFklXETHiXP5Ft87CWeo/tSuwNPFztyTy8XYc74yXdogXKPTJeUHuVzB8Q==", + "requires": { + "@netflix/nerror": "^1.1.3", + "ansi": "^0.3.1", + "bplist-parser": "^0.2.0", + "cross-spawn": "^7.0.1", + "elementtree": "^0.1.7", + "endent": "^1.4.1", + "fast-glob": "^3.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "plist": "^3.0.1", + "q": "^1.5.1", + "read-chunk": "^3.2.0", + "strip-bom": "^4.0.0", + "underscore": "^1.9.2" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "endent": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/endent/-/endent-1.4.1.tgz", + "integrity": "sha512-buHTb5c8AC9NshtP6dgmNLYkiT+olskbq1z6cEGvfGCF3Qphbu/1zz5Xu+yjTDln8RbxNhPoUyJ5H8MSrp1olQ==", + "requires": { + "dedent": "^0.7.0", + "fast-json-parse": "^1.0.3", + "objectorarray": "^1.0.4" + } + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "objectorarray": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/objectorarray/-/objectorarray-1.0.4.tgz", + "integrity": "sha512-91k8bjcldstRz1bG6zJo8lWD7c6QXcB4nTDUqiEvIL1xAsLoZlOOZZG+nd6YPz+V7zY1580J4Xxh1vZtyv4i/w==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" }, "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "requires": { "path-parse": "^1.0.6" } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "underscore": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", + "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } } } }, @@ -3613,47 +4437,229 @@ } }, "cordova-lib": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cordova-lib/-/cordova-lib-9.0.1.tgz", - "integrity": "sha512-P9nQhq91gLOyKZkamvKNzzK89gLDpq8rKue/Vu7NUSgNzhPkiWW0w+6VRTbj/9QGVM9w2uDVhB9c9f6rrTXzCw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/cordova-lib/-/cordova-lib-10.0.0.tgz", + "integrity": "sha512-azU/WH0x/3fQg33tU5bKCtj+Weh/bHelz9FWCVdXqVOHXmjzbi3p6p61z5Si967Tfh3TkmHRrodNxS0ovZ7iFQ==", "requires": { - "cordova-common": "^3.1.0", - "cordova-create": "^2.0.0", - "cordova-fetch": "^2.0.0", - "cordova-serve": "^3.0.0", - "dep-graph": "1.1.0", - "detect-indent": "^5.0.0", + "cordova-common": "^4.0.2", + "cordova-fetch": "^3.0.0", + "cordova-serve": "^4.0.0", + "dep-graph": "^1.1.0", + "detect-indent": "^6.0.0", + "detect-newline": "^3.1.0", "elementtree": "^0.1.7", - "fs-extra": "^7.0.1", - "globby": "^9.1.0", - "indent-string": "^3.2.0", + "execa": "^4.0.3", + "fs-extra": "^9.0.1", + "globby": "^11.0.1", "init-package-json": "^1.10.3", - "md5-file": "^4.0.0", - "read-chunk": "^3.1.0", - "semver": "^5.6.0", - "shebang-command": "^1.2.0", - "underscore": "^1.9.1" + "md5-file": "^5.0.0", + "pify": "^5.0.0", + "semver": "^7.3.2", + "stringify-package": "^1.0.1", + "write-file-atomic": "^3.0.3" }, "dependencies": { - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "big-integer": "^1.6.44" } }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + "cordova-common": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cordova-common/-/cordova-common-4.0.2.tgz", + "integrity": "sha512-od7aNShyuBajzPY83mUEO8tERwwWdFklXETHiXP5Ft87CWeo/tSuwNPFztyTy8XYc74yXdogXKPTJeUHuVzB8Q==", + "requires": { + "@netflix/nerror": "^1.1.3", + "ansi": "^0.3.1", + "bplist-parser": "^0.2.0", + "cross-spawn": "^7.0.1", + "elementtree": "^0.1.7", + "endent": "^1.4.1", + "fast-glob": "^3.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "plist": "^3.0.1", + "q": "^1.5.1", + "read-chunk": "^3.2.0", + "strip-bom": "^4.0.0", + "underscore": "^1.9.2" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "endent": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/endent/-/endent-1.4.1.tgz", + "integrity": "sha512-buHTb5c8AC9NshtP6dgmNLYkiT+olskbq1z6cEGvfGCF3Qphbu/1zz5Xu+yjTDln8RbxNhPoUyJ5H8MSrp1olQ==", + "requires": { + "dedent": "^0.7.0", + "fast-json-parse": "^1.0.3", + "objectorarray": "^1.0.4" + } + }, + "execa": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "objectorarray": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/objectorarray/-/objectorarray-1.0.4.tgz", + "integrity": "sha512-91k8bjcldstRz1bG6zJo8lWD7c6QXcB4nTDUqiEvIL1xAsLoZlOOZZG+nd6YPz+V7zY1580J4Xxh1vZtyv4i/w==" + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "underscore": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", + "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } } } }, @@ -3673,14 +4679,14 @@ "integrity": "sha512-fCLhWjWYn49q3X5xaypAPgTz6MAWSKFFQvD2Gpi5SuVlrRPRphtX2jIqR2zCBuDTBR082QVnlc+yUDXt65Mjgw==" }, "cordova-plugin-chooser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cordova-plugin-chooser/-/cordova-plugin-chooser-1.3.1.tgz", - "integrity": "sha512-xyTgu7T1WSk4XeHVwrez1ZB+iPDThae79OYuuPTJkgHm4fVeD5QzzgJVxo2AETztAOM20OQU6txedfBYB6RHhQ==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/cordova-plugin-chooser/-/cordova-plugin-chooser-1.3.2.tgz", + "integrity": "sha512-GfAibvrPdWe/ri+h3e3xkmq5bietY6yJRBIZawYDE7w600j2mtRsxgat7siWZtjRRhJuVsVwUG6H86Hyp3WKvA==" }, "cordova-plugin-customurlscheme": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-5.0.0.tgz", - "integrity": "sha512-AuVlQyq88cCxGyoYvQyx2N6igLZ5aHxEqGch2vdu1EWvxHzsjj33VFgA3+tjzJUmYYgIN4QYTkyOVsLIMozIPg==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-5.0.1.tgz", + "integrity": "sha512-Nn3+MUrEGfBSFzkC9s5izzOcmpVy8Pya5oYF+CkcdqAlsqL7EqpUan3Q0Eold4EWFisVG5jRCg0XjyxL4uHGfw==" }, "cordova-plugin-device": { "version": "2.0.3", @@ -3693,9 +4699,9 @@ "integrity": "sha512-m7cughw327CjONN/qjzsTpSesLaeybksQh420/gRuSXJX5Zt9NfgsSbqqKDon6jnQ9Mm7h7imgyO2uJ34XMBtA==" }, "cordova-plugin-file-opener2": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cordova-plugin-file-opener2/-/cordova-plugin-file-opener2-3.0.0.tgz", - "integrity": "sha512-yQcJ5coOlfkDcTfIhFJEL2A7SWtLhy50y51Cb+EEkI7Y0lP74Ec2tsMtIOhe9i8wPSoSfnDcN77Hj6CSeIjogA==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/cordova-plugin-file-opener2/-/cordova-plugin-file-opener2-3.0.4.tgz", + "integrity": "sha512-bd1aCx62X2RwpC+KUiuB7quoxL/8RnPMEJU7x38Tvs+cUGLWBvsmR9+/LqGBsSns2CIqgnJ34TW0Vazoqu7Ieg==" }, "cordova-plugin-file-transfer": { "version": "1.7.1", @@ -3712,9 +4718,8 @@ "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4=" }, "cordova-plugin-inappbrowser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-4.0.0.tgz", - "integrity": "sha512-w2LZzdF3R4G/EqVZ9aWch9Pksk76uw6/S5wFP1sgn7zjsSDpJBb/JhazLnioN1NZmZiCUBbROv1S4+9JCkeCgA==" + "version": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#715c858975cc1cb5d140afaa7973938511d38509", + "from": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle" }, "cordova-plugin-ionic-keyboard": { "version": "2.1.3", @@ -3722,14 +4727,18 @@ "integrity": "sha512-6ucQ6FdlLdBm8kJfFnzozmBTjru/0xekHP/dAhjoCZggkGRlgs8TsUJFkxa/bV+qi7Dlo50JjmpE4UMWAO+aOQ==" }, "cordova-plugin-ionic-webview": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-webview/-/cordova-plugin-ionic-webview-4.1.3.tgz", - "integrity": "sha512-hlrUF0kLjjEkZmpYlLJO0NnXmVjMmQ3MOZVXm1ytDihLPKHklYCOpCvjA5Wz3hJrPD1shFEsqi/SPnp873AsdQ==" + "version": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#ac90a8ac88e2c0512d6b250249b1f673f2fbcb68", + "from": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle" }, "cordova-plugin-local-notification": { "version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#0bb96b757fb484553ceabf35a59802f7983a2836", "from": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle" }, + "cordova-plugin-media": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cordova-plugin-media/-/cordova-plugin-media-5.0.3.tgz", + "integrity": "sha512-UQPFlpk1zL4BY44zGi8RVmYCvcKBCN4Dyf8ovxqGYCC8zR1yhbTRWYDdO9vJdERwbfgWV7+z7FMWiSUfqWm9bQ==" + }, "cordova-plugin-media-capture": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/cordova-plugin-media-capture/-/cordova-plugin-media-capture-3.0.3.tgz", @@ -3754,9 +4763,9 @@ "integrity": "sha512-2w6CMC+HGvbhogJetalwGurL2Fx8DQCCPy3wlSZHN1/W7WoQ5n9ujVozcoKrY4VaagK6bxrPFih+ElkO8Uqfzg==" }, "cordova-plugin-splashscreen": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/cordova-plugin-splashscreen/-/cordova-plugin-splashscreen-5.0.3.tgz", - "integrity": "sha512-rnoDXMDfzoeHDBvsnu6JmzDE/pV5YJCAfc5hYX/Mb2BIXGgSjFJheByt0tU6kp3Wl40tSyFX4pYfBwFblBGyRg==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cordova-plugin-splashscreen/-/cordova-plugin-splashscreen-6.0.0.tgz", + "integrity": "sha512-pm4ZtJKQY4bCGXVeIInbGrXilryTevYSKgfvoQJpW9UClOWKAxSsYf2/4G2u1vcn492svOSL42OSa2MhujBWEQ==" }, "cordova-plugin-statusbar": { "version": "2.4.3", @@ -3769,7 +4778,7 @@ "integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ==" }, "cordova-plugin-wkuserscript": { - "version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#6413f4bb3c2565f353e690b5c1450b69ad9e860e", + "version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#1ad47e75a1811cec0a944d3b8b8544b3d5e052ca", "from": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git" }, "cordova-plugin-wkwebview-cookies": { @@ -3782,15 +4791,307 @@ "integrity": "sha1-F2yCSOog058c+VnvXmFWrMqWshc=" }, "cordova-serve": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cordova-serve/-/cordova-serve-3.0.0.tgz", - "integrity": "sha512-h479g/5a0PXn//yiFuMrD5MDEbB+mtihNkWcE6uD/aCh/6z0FRZ9sWH3NfZbHDB+Bp1yGLYsjbH8LZBL8KOQ0w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cordova-serve/-/cordova-serve-4.0.0.tgz", + "integrity": "sha512-gzTLeBQzNP8aM/nG0/7sSfICfNazUgwvEU2kiDaybbYXmxwioo2v96h4tzE0XOyA64beyYwAyRYEEqWA4AMZjw==", "requires": { - "chalk": "^2.4.1", - "compression": "^1.6.0", - "express": "^4.13.3", - "opn": "^5.3.0", - "which": "^1.3.0" + "chalk": "^3.0.0", + "compression": "^1.7.4", + "express": "^4.17.1", + "open": "^7.0.3", + "which": "^2.0.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } } }, "cordova-sqlite-storage": { @@ -3813,6 +5114,25 @@ "resolved": "https://registry.npmjs.org/cordova-support-google-services/-/cordova-support-google-services-1.3.2.tgz", "integrity": "sha512-RtEWzULreUX662MFWopGhFispLiHX7gUf2GijPOC2mY2oCNuUobj2mO4tl5q7PYbOreSxq+PrSekhmS6TAAWdw==" }, + "cordova.plugins.diagnostic": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/cordova.plugins.diagnostic/-/cordova.plugins.diagnostic-5.0.2.tgz", + "integrity": "sha512-H59o7YxJ2/COzvg+jyTpUqX8QoDcvti9dluJ9a+pHumE8lf3meWemwCl0QFa9GH+xgVd6X1Ikj/6P3+DKWd9eg==", + "dev": true, + "requires": { + "colors": "^1.1.2", + "elementtree": "^0.1.6", + "minimist": "1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "core-js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", @@ -3833,14 +5153,6 @@ "elliptic": "^6.0.0" } }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -3872,6 +5184,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -3898,9 +5211,9 @@ } }, "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" }, "currently-unhandled": { "version": "0.4.1", @@ -3963,7 +5276,17 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "requires": { + "mimic-response": "^2.0.0" + } }, "dedent": { "version": "0.7.0", @@ -4026,6 +5349,11 @@ } } }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, "define-properties": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", @@ -4039,6 +5367,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, "requires": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" @@ -4048,6 +5377,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -4056,6 +5386,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -4064,6 +5395,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -4073,7 +5405,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -4109,8 +5442,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "dep-graph": { "version": "1.1.0", @@ -4154,15 +5486,19 @@ "dev": true }, "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz", + "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==" }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" }, "diff": { "version": "3.5.0", @@ -4182,25 +5518,17 @@ } }, "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "requires": { - "path-type": "^3.0.0" + "path-type": "^4.0.0" }, "dependencies": { "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" } } }, @@ -4235,11 +5563,11 @@ "dev": true }, "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "requires": { - "is-obj": "^1.0.0" + "is-obj": "^2.0.0" } }, "dotenv": { @@ -4410,7 +5738,7 @@ }, "electron-osx-sign": { "version": "0.4.10", - "resolved": "http://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz", "integrity": "sha1-vk87ibKnWh3F8eckkIGrKSnKOiY=", "dev": true, "requires": { @@ -4441,7 +5769,7 @@ }, "xmlbuilder": { "version": "8.2.2", - "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", "dev": true } @@ -4869,9 +6197,9 @@ } }, "yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==", + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -4902,9 +6230,9 @@ } }, "elliptic": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", - "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -4919,8 +6247,7 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "emojis-list": { "version": "2.1.0", @@ -5116,6 +6443,11 @@ "es6-symbol": "^3.1.1" } }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -5127,9 +6459,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", - "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "dev": true, "requires": { "esprima": "^4.0.1", @@ -5219,6 +6551,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -5245,6 +6578,12 @@ "fill-range": "^2.1.0" } }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "optional": true + }, "expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -5258,6 +6597,7 @@ "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "dev": true, "requires": { "accepts": "~1.3.5", "array-flatten": "1.1.1", @@ -5294,20 +6634,21 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true } } }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -5317,6 +6658,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -5368,235 +6710,40 @@ } }, "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" }, "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "fill-range": "^7.0.1" } }, "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "to-regex-range": "^5.0.1" } }, "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-glob": "^4.0.1" } }, "is-extglob": { @@ -5613,51 +6760,25 @@ } }, "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" } } } @@ -5684,6 +6805,14 @@ "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", "dev": true }, + "fastq": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", + "requires": { + "reusify": "^1.0.4" + } + }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -5703,9 +6832,9 @@ } }, "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "requires": { "escape-string-regexp": "^1.0.5" } @@ -5736,6 +6865,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dev": true, "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -6125,13 +7255,25 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + } } }, "formidable": { @@ -6149,6 +7291,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, "requires": { "map-cache": "^0.2.2" } @@ -6158,6 +7301,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, "fs-extra": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", @@ -6196,7 +7345,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, "requires": { "minipass": "^2.6.0" } @@ -6209,6 +7357,18 @@ "requires": { "graceful-fs": "^4.1.11", "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "fs.realpath": { @@ -6271,12 +7431,6 @@ "concat-map": "0.0.1" } }, - "chownr": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "optional": true - }, "code-point-at": { "version": "1.1.0", "resolved": false, @@ -6324,27 +7478,6 @@ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, - "fs-minipass": { - "version": "1.2.5", - "resolved": false, - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "optional": true, - "requires": { - "minipass": "^2.2.1" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, "fs.realpath": { "version": "1.0.0", "resolved": false, @@ -6465,40 +7598,19 @@ "yallist": "^3.0.0" } }, - "minizlib": { - "version": "1.2.1", - "resolved": false, - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "optional": true, - "requires": { - "minipass": "^2.2.1" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, "mkdirp": { - "version": "0.5.1", - "resolved": false, - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "optional": true, "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" }, "dependencies": { "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "optional": true } } @@ -6663,9 +7775,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "optional": true } } @@ -6765,18 +7877,18 @@ "optional": true }, "tar": { - "version": "4.4.8", - "resolved": false, - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "yallist": "^3.0.3" }, "dependencies": { "minipass": { @@ -6813,9 +7925,9 @@ "optional": true }, "yallist": { - "version": "3.0.3", - "resolved": false, - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, @@ -6876,7 +7988,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -6912,7 +8023,8 @@ "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true }, "get-uri": { "version": "2.0.4", @@ -6931,7 +8043,8 @@ "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true }, "getpass": { "version": "0.1.7", @@ -6948,6 +8061,12 @@ } } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "optional": true + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -7023,11 +8142,6 @@ } } }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" - }, "glob-watcher": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", @@ -7557,7 +8671,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -7723,7 +8837,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true @@ -7839,7 +8953,7 @@ }, "tar": { "version": "4.4.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, @@ -8024,11 +9138,11 @@ } }, "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", + "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", "requires": { - "ini": "^1.3.4" + "ini": "^1.3.5" } }, "global-modules": { @@ -8056,49 +9170,29 @@ } }, "globby": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", - "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", "requires": { - "@types/glob": "^7.1.1", - "array-union": "^1.0.2", - "dir-glob": "^2.2.2", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" }, "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" } } }, "globule": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.0.tgz", - "integrity": "sha512-YlD4kdMqRCQHrhVdonet4TdRtv1/sZKepvoxNT4Nrhrp5HI8XFfc8kFlGlBn2myBo80aGp8Eft259mbcUJhgSg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz", + "integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==", "dev": true, "requires": { "glob": "~7.1.1", @@ -8116,21 +9210,53 @@ } }, "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", "requires": { - "create-error-class": "^3.0.0", + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } } }, "graceful-fs": { @@ -8191,6 +9317,18 @@ "dev": true, "requires": { "through2": "~2.0.1" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "gulp-concat": { @@ -8202,6 +9340,18 @@ "concat-with-sourcemaps": "^1.0.0", "through2": "^2.0.0", "vinyl": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "gulp-flatten": { @@ -8212,6 +9362,18 @@ "requires": { "plugin-error": "^0.1.2", "through2": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "gulp-htmlmin": { @@ -8242,6 +9404,16 @@ "arr-union": "^3.1.0", "extend-shallow": "^3.0.2" } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } } } }, @@ -8324,29 +9496,6 @@ "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", - "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } } }, "has": { @@ -8380,13 +9529,13 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, "requires": { "get-value": "^2.0.6", "has-values": "^1.0.0", @@ -8396,7 +9545,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -8404,6 +9554,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, "requires": { "is-number": "^3.0.0", "kind-of": "^4.0.0" @@ -8413,6 +9564,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -8421,6 +9573,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -8431,12 +9584,18 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } } } }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, "hash-base": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", @@ -8529,10 +9688,16 @@ } } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -8610,10 +9775,16 @@ } } }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -8621,13 +9792,12 @@ "ieee754": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", - "dev": true + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" }, "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" }, "immediate": { "version": "3.0.6", @@ -8635,9 +9805,9 @@ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" }, "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -8654,9 +9824,9 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, "in-publish": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz", + "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", "dev": true }, "indent-string": { @@ -8706,28 +9876,201 @@ "semver": "2.x || 3.x || 4 || 5", "validate-npm-package-license": "^3.0.1", "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + }, + "npm-package-arg": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + } } }, "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.2.tgz", + "integrity": "sha512-DF4osh1FM6l0RJc5YWYhSDB6TawiBRlbV9Cox8MWlidU218Tb7fm3lQTULyUJDfJ0tjbzl0W4q651mrCCEM55w==", "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", + "figures": "^3.0.0", + "lodash": "^4.17.16", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rxjs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", + "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "insight": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/insight/-/insight-0.10.3.tgz", + "integrity": "sha512-YOncxSN6Omh+1Oqxt+OJAvJVMDKw7l6IEG0wT2cTMGxjsTcroOGW4IR926QDzxg/uZHcFZ2cZbckDWdZhc2pZw==", + "requires": { + "async": "^2.6.2", + "chalk": "^2.4.2", + "conf": "^1.4.0", + "inquirer": "^6.3.1", + "lodash.debounce": "^4.0.8", + "os-name": "^3.1.0", + "request": "^2.88.0", + "tough-cookie": "^3.0.1", + "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -8743,20 +10086,67 @@ "supports-color": "^5.3.0" } }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, + "macos-release": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz", + "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==" + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, + "os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "requires": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "requires": { "tslib": "^1.9.0" } @@ -8794,53 +10184,6 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" } } - } - } - }, - "insight": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/insight/-/insight-0.10.3.tgz", - "integrity": "sha512-YOncxSN6Omh+1Oqxt+OJAvJVMDKw7l6IEG0wT2cTMGxjsTcroOGW4IR926QDzxg/uZHcFZ2cZbckDWdZhc2pZw==", - "requires": { - "async": "^2.6.2", - "chalk": "^2.4.2", - "conf": "^1.4.0", - "inquirer": "^6.3.1", - "lodash.debounce": "^4.0.8", - "os-name": "^3.1.0", - "request": "^2.88.0", - "tough-cookie": "^3.0.1", - "uuid": "^3.3.2" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "macos-release": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", - "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==" - }, - "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "requires": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "tough-cookie": { "version": "3.0.1", @@ -8921,7 +10264,8 @@ "ipaddr.js": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", + "dev": true }, "is-absolute": { "version": "1.0.0", @@ -8937,6 +10281,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -8945,6 +10290,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -8987,6 +10333,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "dev": true, "requires": { "ci-info": "^1.0.0" } @@ -8995,6 +10342,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -9003,6 +10351,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -9018,6 +10367,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -9027,15 +10377,15 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true } } }, "is-docker": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", - "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", - "dev": true + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==" }, "is-dotfile": { "version": "1.0.3", @@ -9061,19 +10411,15 @@ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" }, "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9087,12 +10433,12 @@ } }, "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" } }, "is-module": { @@ -9108,9 +10454,9 @@ "dev": true }, "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" }, "is-number": { "version": "2.1.0", @@ -9131,14 +10477,15 @@ } }, "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" }, "is-odd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, "requires": { "is-number": "^4.0.0" }, @@ -9146,22 +10493,21 @@ "is-number": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true } } }, "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" - } + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" }, @@ -9169,7 +10515,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -9183,16 +10530,6 @@ "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" - }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -9210,11 +10547,6 @@ "is-unc-path": "^1.0.0" } }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" - }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -9239,11 +10571,6 @@ "unc-path-regex": "^0.1.2" } }, - "is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" - }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -9259,12 +10586,13 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" }, "isarray": { "version": "1.0.0", @@ -9299,9 +10627,9 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "js-base64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.3.tgz", + "integrity": "sha512-fiUvdfCaAXoQTHdKMgTvg6IkecXDcVz6V5rlftUTclF9IKBjMizvSdQaCl/z/6TApDeby5NL+axYou3i0mu1Pg==", "dev": true }, "js-tokens": { @@ -9326,16 +10654,21 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "optional": true }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, "json-loader": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", "dev": true }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema": { "version": "0.2.3", @@ -9436,10 +10769,29 @@ "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", "dev": true }, + "keytar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-6.0.1.tgz", + "integrity": "sha512-1Ihpf2tdM3sLwGMkYHXYhVC/hx5BDR7CWFL4IrBA3IDZo0xHhS2nM+tU9Y+u/U7okNfbVkwmKsieLkcWRMh93g==", + "optional": true, + "requires": { + "node-addon-api": "^3.0.0", + "prebuild-install": "5.3.4" + } + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true }, "last-run": { "version": "1.1.1", @@ -9452,11 +10804,11 @@ } }, "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", "requires": { - "package-json": "^4.0.0" + "package-json": "^6.3.0" } }, "lazy-cache": { @@ -9609,9 +10961,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash._baseassign": { "version": "3.2.0", @@ -9732,137 +11084,6 @@ "chalk": "^2.0.1" } }, - "log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "requires": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -9894,6 +11115,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -9915,17 +11137,17 @@ } }, "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "requires": { - "pify": "^3.0.0" + "semver": "^6.0.0" }, "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -9949,7 +11171,8 @@ "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true }, "map-obj": { "version": "1.0.1", @@ -9961,6 +11184,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, "requires": { "object-visit": "^1.0.0" } @@ -10291,9 +11515,9 @@ "integrity": "sha512-OOl0B2/0tSJAtAZarXnQuLDBLgTNRqiI9VqHTQzPsxf4okT2iIpDrvaklK9x2QEMD1sDj4yRn11Ygci41DxMAQ==" }, "md5-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-4.0.0.tgz", - "integrity": "sha512-UC0qFwyAjn4YdPpKaDNw6gNxRf7Mcx7jC1UGCY4boCzgvU2Aoc1mOGzTtrjjLKhM5ivsnhoKpQVxKPp+1j1qwg==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", + "integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw==" }, "md5.js": { "version": "1.3.5", @@ -10353,10 +11577,15 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "methods": { "version": "1.1.2", @@ -10406,7 +11635,8 @@ "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true }, "mime-db": { "version": "1.38.0", @@ -10426,6 +11656,12 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -10455,7 +11691,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -10464,8 +11699,7 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, @@ -10473,7 +11707,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, "requires": { "minipass": "^2.9.0" } @@ -10482,6 +11715,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, "requires": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" @@ -10491,6 +11725,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -10505,6 +11740,12 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "optional": true + }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", @@ -10530,13 +11771,13 @@ "version": "2.14.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "dev": true, - "optional": true + "dev": true }, "nanomatch": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -10555,20 +11796,29 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true } } }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "optional": true + }, "native-run": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/native-run/-/native-run-1.0.0.tgz", @@ -10624,9 +11874,9 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, "netmask": { @@ -10660,6 +11910,21 @@ "lower-case": "^1.1.1" } }, + "node-abi": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.18.0.tgz", + "integrity": "sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw==", + "optional": true, + "requires": { + "semver": "^5.4.1" + } + }, + "node-addon-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", + "integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg==", + "optional": true + }, "node-gyp": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", @@ -10680,179 +11945,11 @@ "which": "1" }, "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" - } - }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "dev": true, - "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==", - "dev": true - }, - "mime-types": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", - "dev": true, - "requires": { - "mime-db": "~1.36.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true } } }, @@ -10929,9 +12026,9 @@ "dev": true }, "node-sass": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz", - "integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", + "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -10948,7 +12045,7 @@ "node-gyp": "^3.8.0", "npmlog": "^4.0.0", "request": "^2.88.0", - "sass-graph": "^2.2.4", + "sass-graph": "2.2.5", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" }, @@ -10982,12 +12079,6 @@ "which": "^1.2.9" } }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true - }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -10996,6 +12087,12 @@ } } }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", + "optional": true + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -11030,6 +12127,11 @@ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", "dev": true }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, "now-and-later": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", @@ -11039,26 +12141,46 @@ "once": "^1.3.2" } }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + }, "npm-package-arg": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", - "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.1.tgz", + "integrity": "sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ==", "requires": { - "hosted-git-info": "^2.7.1", - "osenv": "^0.1.5", - "semver": "^5.6.0", + "hosted-git-info": "^3.0.2", + "semver": "^7.0.0", "validate-npm-package-name": "^3.0.0" }, "dependencies": { "hosted-git-info": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", - "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.5.tgz", + "integrity": "sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -11074,7 +12196,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -11091,8 +12212,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.9.0", @@ -11102,13 +12222,13 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, "requires": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", @@ -11119,6 +12239,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -11127,6 +12248,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -11147,6 +12269,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, "requires": { "isobject": "^3.0.0" }, @@ -11154,7 +12277,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -11233,6 +12357,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, "requires": { "isobject": "^3.0.1" }, @@ -11240,7 +12365,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -11306,7 +12432,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz", "integrity": "sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ==", - "dev": true, "requires": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -11316,21 +12441,12 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, "requires": { "is-docker": "^2.0.0" } } } }, - "opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", - "requires": { - "is-wsl": "^1.1.0" - } - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -11398,6 +12514,11 @@ "os-tmpdir": "^1.0.0" } }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -11471,14 +12592,21 @@ } }, "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "pako": { @@ -11557,12 +12685,14 @@ "parseurl": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true }, "path-browserify": { "version": "0.0.0", @@ -11573,7 +12703,8 @@ "path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true }, "path-exists": { "version": "2.1.0", @@ -11683,6 +12814,11 @@ "install": "^0.8.2" } }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -11787,7 +12923,8 @@ "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true }, "postcss": { "version": "6.0.23", @@ -11806,6 +12943,41 @@ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", "dev": true }, + "prebuild-install": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.4.tgz", + "integrity": "sha512-AkKN+pf4fSEihjapLEEj8n85YIw/tN6BQqkhzbDc0RvEZGdkpJBGMUYx66AAMcPG2KzmPQS7Cm16an4HVBRRMA==", + "optional": true, + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -11813,9 +12985,9 @@ "dev": true }, "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" }, "preserve": { "version": "0.2.0", @@ -11869,6 +13041,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "dev": true, "requires": { "forwarded": "~0.1.2", "ipaddr.js": "1.8.0" @@ -11943,7 +13116,8 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true }, "psl": { "version": "1.1.29", @@ -11988,7 +13162,16 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "pupa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", + "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", + "requires": { + "escape-goat": "^2.0.0" + } }, "q": { "version": "1.5.1", @@ -12061,12 +13244,14 @@ "range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true }, "raw-body": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "dev": true, "requires": { "bytes": "3.0.0", "http-errors": "1.6.3", @@ -12168,22 +13353,14 @@ } }, "read-package-json": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.0.tgz", - "integrity": "sha512-KLhu8M1ZZNkMcrq1+0UJbR8Dii8KZUqB0Sha4mOx/bknfKI/fyrQVrG/YIt2UOtG667sD8+ee4EXMM91W9dC+A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", "requires": { "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "normalize-package-data": "^2.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - } + "npm-normalize-package-bin": "^1.0.0" } }, "read-pkg": { @@ -12268,26 +13445,26 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, "requires": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" } }, "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", + "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" + "rc": "^1.2.8" } }, "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", "requires": { - "rc": "^1.0.1" + "rc": "^1.2.8" } }, "relateurl": { @@ -12315,6 +13492,18 @@ "remove-bom-buffer": "^3.0.0", "safe-buffer": "^5.1.0", "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "remove-trailing-separator": { @@ -12359,9 +13548,9 @@ } }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -12370,7 +13559,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -12380,15 +13569,20 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, "dependencies": { - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } } } }, @@ -12440,7 +13634,16 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } }, "restore-cursor": { "version": "2.0.0", @@ -12462,7 +13665,13 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "right-align": { "version": "0.1.3", @@ -12474,12 +13683,28 @@ } }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "ripemd160": { @@ -12822,12 +14047,14 @@ "dev": true }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" + }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" }, "rxjs": { "version": "5.5.12", @@ -12852,6 +14079,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, "requires": { "ret": "~0.1.10" } @@ -12871,15 +14099,178 @@ } }, "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", + "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", "dev": true, "requires": { "glob": "^7.0.0", "lodash": "^4.0.0", "scss-tokenizer": "^0.2.3", - "yargs": "^7.0.0" + "yargs": "^13.3.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } } }, "sax": { @@ -12920,11 +14311,18 @@ "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", "requires": { - "semver": "^5.0.3" + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "semver-greatest-satisfied-range": { @@ -12940,6 +14338,7 @@ "version": "0.16.2", "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, "requires": { "debug": "2.6.9", "depd": "~1.1.2", @@ -12960,6 +14359,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -12975,8 +14375,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-immediate-shim": { "version": "1.0.1", @@ -12987,6 +14386,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -12998,6 +14398,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -13013,7 +14414,8 @@ "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true }, "sha.js": { "version": "2.4.11", @@ -13064,6 +14466,23 @@ } } }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "optional": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "optional": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "simple-plist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.0.0.tgz", @@ -13134,6 +14553,7 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, "requires": { "base": "^0.11.1", "debug": "^2.2.0", @@ -13149,6 +14569,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -13157,6 +14578,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -13164,7 +14586,8 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true } } }, @@ -13172,6 +14595,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, "requires": { "define-property": "^1.0.0", "isobject": "^3.0.0", @@ -13182,6 +14606,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -13190,6 +14615,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -13198,6 +14624,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -13206,6 +14633,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -13215,12 +14643,14 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true } } }, @@ -13228,6 +14658,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, "requires": { "kind-of": "^3.2.0" }, @@ -13236,6 +14667,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -13288,6 +14720,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, "requires": { "atob": "^2.1.1", "decode-uri-component": "^0.2.0", @@ -13308,7 +14741,8 @@ "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true }, "sparkles": { "version": "1.0.1", @@ -13348,6 +14782,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, "requires": { "extend-shallow": "^3.0.0" } @@ -13425,6 +14860,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, "requires": { "define-property": "^0.2.5", "object-copy": "^0.1.0" @@ -13434,6 +14870,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -13443,7 +14880,8 @@ "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true }, "stdout-stream": { "version": "1.4.1", @@ -13514,7 +14952,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -13544,11 +14981,15 @@ "safe-buffer": "~5.1.0" } }, + "stringify-package": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz", + "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==" + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -13567,6 +15008,11 @@ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, "strip-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", @@ -13591,9 +15037,9 @@ } }, "superagent": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.2.2.tgz", - "integrity": "sha512-pMWBUnIllK4ZTw7p/UaobiQPwAO5w/1NRRTDpV0FTVNmECztsxKspj3ZWEordVEaqpZtmOQJJna4yTLyC/q7PQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz", + "integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==", "dev": true, "requires": { "component-emitter": "^1.3.0", @@ -13601,23 +15047,14 @@ "debug": "^4.1.1", "fast-safe-stringify": "^2.0.7", "form-data": "^3.0.0", - "formidable": "^1.2.1", + "formidable": "^1.2.2", "methods": "^1.1.2", - "mime": "^2.4.4", - "qs": "^6.9.1", - "readable-stream": "^3.4.0", - "semver": "^6.3.0" + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" }, "dependencies": { - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -13633,17 +15070,6 @@ "ms": "^2.1.1" } }, - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "mime": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", @@ -13674,9 +15100,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true } } @@ -13740,6 +15166,11 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" }, + "systeminformation": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-4.27.3.tgz", + "integrity": "sha512-0Nc8AYEK818h7FI+bbe/kj7xXsMD5zOHvO9alUqQH/G4MHXu5tHQfWqC/bzWOk4JtoQPhnyLgxMYncDA2eeSBw==" + }, "tail": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/tail/-/tail-0.4.0.tgz", @@ -13813,18 +15244,54 @@ "block-stream": "*", "fstream": "^1.0.12", "inherits": "2" + } + }, + "tar-fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", + "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", + "optional": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" }, "dependencies": { - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "tar-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz", + "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==", + "optional": true, + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } } } @@ -13842,28 +15309,15 @@ } }, "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "requires": { - "execa": "^0.7.0" - } + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", + "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "dev": true, - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } - }, "through2-filter": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", @@ -13872,6 +15326,18 @@ "requires": { "through2": "~2.0.0", "xtend": "~4.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "thunkify": { @@ -13886,11 +15352,6 @@ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" - }, "timers-browserify": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", @@ -13959,6 +15420,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -13967,16 +15429,23 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } } } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, "requires": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", @@ -13988,6 +15457,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -13997,6 +15467,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -14005,6 +15476,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -14020,15 +15492,39 @@ "dev": true, "requires": { "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } } }, "tree-kill": { @@ -14162,13 +15658,13 @@ "type-fest": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" }, "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, "requires": { "media-typer": "0.3.0", "mime-types": "~2.1.18" @@ -14184,7 +15680,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, "requires": { "is-typedarray": "^1.0.0" } @@ -14323,6 +15818,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -14341,11 +15837,11 @@ } }, "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "requires": { - "crypto-random-string": "^1.0.0" + "crypto-random-string": "^2.0.0" } }, "universalify": { @@ -14367,6 +15863,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, "requires": { "has-value": "^0.3.1", "isobject": "^3.0.0" @@ -14376,6 +15873,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, "requires": { "get-value": "^2.0.3", "has-values": "^0.1.4", @@ -14386,6 +15884,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, "requires": { "isarray": "1.0.0" } @@ -14395,12 +15894,14 @@ "has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -14410,45 +15911,88 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" - }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true + "dev": true, + "optional": true }, "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.1.tgz", + "integrity": "sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg==", "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" }, "dependencies": { - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" } } } @@ -14477,7 +16021,8 @@ "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true }, "url": { "version": "0.11.0", @@ -14498,17 +16043,18 @@ } }, "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", "requires": { - "prepend-http": "^1.0.1" + "prepend-http": "^2.0.0" } }, "use": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, "requires": { "kind-of": "^6.0.2" }, @@ -14516,7 +16062,8 @@ "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true } } }, @@ -14646,6 +16193,18 @@ "value-or-function": "^3.0.0", "vinyl": "^2.0.0", "vinyl-sourcemap": "^1.1.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, "vinyl-sourcemap": { @@ -14679,14 +16238,160 @@ } }, "watchpack": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", - "integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", + "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "dev": true, "requires": { - "chokidar": "^2.1.8", + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true, + "optional": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz", + "integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "optional": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "watchpack-chokidar2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", + "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" }, "dependencies": { "anymatch": { @@ -14694,6 +16399,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, + "optional": true, "requires": { "micromatch": "^3.1.4", "normalize-path": "^2.1.1" @@ -14704,6 +16410,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, + "optional": true, "requires": { "remove-trailing-separator": "^1.0.1" } @@ -14714,19 +16421,22 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true + "dev": true, + "optional": true }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true + "dev": true, + "optional": true }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, + "optional": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -14745,6 +16455,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -14756,6 +16467,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, + "optional": true, "requires": { "anymatch": "^2.0.0", "async-each": "^1.0.1", @@ -14776,6 +16488,7 @@ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dev": true, + "optional": true, "requires": { "debug": "^2.3.3", "define-property": "^0.2.5", @@ -14791,6 +16504,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, + "optional": true, "requires": { "is-descriptor": "^0.1.0" } @@ -14800,6 +16514,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -14809,6 +16524,7 @@ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -14818,6 +16534,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -14829,6 +16546,7 @@ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -14838,6 +16556,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -14849,6 +16568,7 @@ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, + "optional": true, "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -14859,7 +16579,8 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true + "dev": true, + "optional": true } } }, @@ -14868,6 +16589,7 @@ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "dev": true, + "optional": true, "requires": { "array-unique": "^0.3.2", "define-property": "^1.0.0", @@ -14884,6 +16606,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, + "optional": true, "requires": { "is-descriptor": "^1.0.0" } @@ -14893,6 +16616,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -14904,6 +16628,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, + "optional": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -14916,631 +16641,19 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } } } }, - "fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": false, - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": false, - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "resolved": false, - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": false, - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": false, - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "resolved": false, - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": false, - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": false, - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "resolved": false, - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": false, - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "resolved": false, - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": false, - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": false, - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.4", - "resolved": false, - "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.3", - "resolved": false, - "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "resolved": false, - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-7Mni4Z8Xkx0/oegoqlcao/JpPCPEMtUvsmB0q7mgvlMinykJLSRTYuFqoQLYgGY8biuxIeiHO+QNJKbCfljewQ==", - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": false, - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": false, - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": false, - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": false, - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "resolved": false, - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": false, - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": false, - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": false, - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "resolved": false, - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "optional": true - }, - "semver": { - "version": "5.6.0", - "resolved": false, - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "resolved": false, - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": false, - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "resolved": false, - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true - } - } - }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, + "optional": true, "requires": { "is-glob": "^3.1.0", "path-dirname": "^1.0.0" @@ -15551,6 +16664,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, + "optional": true, "requires": { "is-extglob": "^2.1.0" } @@ -15562,6 +16676,7 @@ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, + "optional": true, "requires": { "kind-of": "^6.0.0" } @@ -15571,6 +16686,7 @@ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, + "optional": true, "requires": { "kind-of": "^6.0.0" } @@ -15580,6 +16696,7 @@ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, + "optional": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -15590,13 +16707,15 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "dev": true, + "optional": true }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, + "optional": true, "requires": { "is-extglob": "^2.1.1" } @@ -15606,6 +16725,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -15615,6 +16735,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -15625,13 +16746,15 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "dev": true, + "optional": true }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, + "optional": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -15652,13 +16775,15 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "dev": true, + "optional": true }, "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dev": true, + "optional": true, "requires": { "graceful-fs": "^4.1.11", "micromatch": "^3.1.10", @@ -15962,48 +17087,59 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "optional": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, "requires": { "string-width": "^1.0.2 || 2" } }, "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", "requires": { - "string-width": "^2.1.1" + "string-width": "^4.0.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.0" } } } @@ -16024,9 +17160,9 @@ "dev": true }, "windows-release": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", - "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", "requires": { "execa": "^1.0.0" }, @@ -16077,9 +17213,9 @@ } }, "with-open-file": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.6.tgz", - "integrity": "sha512-SQS05JekbtwQSgCYlBsZn/+m2gpn4zWsqpCYIrCHva0+ojXcnmUEPsBN6Ipoz3vmY/81k5PvYEWSxER2g4BTqA==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz", + "integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==", "requires": { "p-finally": "^1.0.0", "p-try": "^2.1.0", @@ -16126,13 +17262,14 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "requires": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, "ws": { @@ -16156,9 +17293,9 @@ } }, "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" }, "xml-escape": { "version": "1.1.0", @@ -16206,7 +17343,8 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true }, "yargs": { "version": "7.1.0", diff --git a/package.json b/package.json index bd73881a4..a4f73ed0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.9.2", + "version": "3.9.3", "description": "The official app for Moodle.", "author": { "name": "Moodle Pty Ltd.", @@ -57,19 +57,21 @@ "@angular/platform-browser-dynamic": "5.2.11", "@ionic-native/badge": "4.20.0", "@ionic-native/camera": "4.20.0", - "@ionic-native/chooser": "^4.20.0", + "@ionic-native/chooser": "4.20.0", "@ionic-native/clipboard": "4.20.0", "@ionic-native/core": "4.20.0", "@ionic-native/device": "4.20.0", + "@ionic-native/diagnostic": "4.2.0", "@ionic-native/file": "4.20.0", "@ionic-native/file-opener": "4.20.0", "@ionic-native/file-transfer": "4.20.0", "@ionic-native/geolocation": "4.20.0", "@ionic-native/globalization": "4.20.0", - "@ionic-native/http": "^4.20.0", + "@ionic-native/http": "4.20.0", "@ionic-native/in-app-browser": "4.20.0", "@ionic-native/keyboard": "4.20.0", "@ionic-native/local-notifications": "4.20.0", + "@ionic-native/media": "4.20.0", "@ionic-native/media-capture": "4.20.0", "@ionic-native/network": "4.20.0", "@ionic-native/push": "4.20.0", @@ -85,7 +87,7 @@ "ajv": "6.11.0", "chart.js": "2.9.3", "com-darryncampbell-cordova-plugin-intent": "1.3.0", - "cordova": "9.0.0", + "cordova": "10.0.0", "cordova-android": "8.1.0", "cordova-android-support-gradle-release": "3.0.1", "cordova-clipboard": "1.3.0", @@ -93,23 +95,24 @@ "cordova-plugin-advanced-http": "2.4.1", "cordova-plugin-badge": "0.8.8", "cordova-plugin-camera": "4.1.0", - "cordova-plugin-chooser": "1.3.1", - "cordova-plugin-customurlscheme": "5.0.0", + "cordova-plugin-chooser": "1.3.2", + "cordova-plugin-customurlscheme": "5.0.1", "cordova-plugin-device": "2.0.3", "cordova-plugin-file": "6.0.2", - "cordova-plugin-file-opener2": "3.0.0", + "cordova-plugin-file-opener2": "3.0.4", "cordova-plugin-file-transfer": "1.7.1", "cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", "cordova-plugin-globalization": "1.11.0", - "cordova-plugin-inappbrowser": "4.0.0", + "cordova-plugin-inappbrowser": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle", "cordova-plugin-ionic-keyboard": "2.1.3", - "cordova-plugin-ionic-webview": "4.1.3", + "cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle", "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", + "cordova-plugin-media": "5.0.3", "cordova-plugin-media-capture": "3.0.3", "cordova-plugin-network-information": "2.0.2", "cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist", "cordova-plugin-screen-orientation": "3.0.2", - "cordova-plugin-splashscreen": "5.0.3", + "cordova-plugin-splashscreen": "6.0.0", "cordova-plugin-statusbar": "2.4.3", "cordova-plugin-whitelist": "1.3.4", "cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git", @@ -119,6 +122,7 @@ "cordova-support-google-services": "1.3.2", "es6-promise-plugin": "4.2.2", "font-awesome": "4.7.0", + "inquirer": "^7.3.2", "ionic-angular": "3.9.9", "ionicons": "3.0.0", "jszip": "3.1.5", @@ -136,31 +140,36 @@ }, "devDependencies": { "@ionic/app-scripts": "3.2.3", - "@ionic/cli": "^6.9.3", - "@types/cordova": "0.0.34", - "@types/cordova-plugin-file-transfer": "0.0.3", - "@types/cordova-plugin-globalization": "0.0.3", - "@types/cordova-plugin-network-information": "0.0.3", - "@types/node": "8.10.59", - "@types/promise.prototype.finally": "2.0.4", + "@ionic/cli": "^6.11.7", + "@types/cordova": "^0.0.34", + "@types/cordova-plugin-file-transfer": "^0.0.3", + "@types/cordova-plugin-globalization": "^0.0.3", + "@types/cordova-plugin-network-information": "^0.0.3", + "@types/node": "^8.10.59", + "@types/promise.prototype.finally": "^2.0.4", "acorn": "^5.7.4", - "electron-builder-lib": "20.23.1", - "electron-rebuild": "1.10.0", + "cordova.plugins.diagnostic": "^5.0.2", + "electron-builder-lib": "^20.23.1", + "electron-rebuild": "^1.10.0", "gulp": "4.0.2", - "gulp-clip-empty-files": "0.1.2", - "gulp-concat": "2.6.1", - "gulp-flatten": "0.4.0", - "gulp-htmlmin": "5.0.1", - "gulp-rename": "2.0.0", - "gulp-slash": "1.1.3", - "lodash.template": "4.5.0", + "gulp-clip-empty-files": "^0.1.2", + "gulp-concat": "^2.6.1", + "gulp-flatten": "^0.4.0", + "gulp-htmlmin": "^5.0.1", + "gulp-rename": "^2.0.0", + "gulp-slash": "^1.1.3", + "lodash.template": "^4.5.0", "minimist": "^1.2.5", "native-run": "^1.0.0", - "node-loader": "0.6.0", - "through": "2.3.8", - "typescript": "2.6.2", - "vinyl": "2.2.0", - "webpack-merge": "4.2.2" + "node-loader": "^0.6.0", + "request": "^2.88.2", + "through": "^2.3.8", + "typescript": "~2.6.2", + "vinyl": "^2.2.0", + "webpack-merge": "^4.2.2" + }, + "optionalDependencies": { + "keytar": "^6.0.1" }, "browser": { "electron": false @@ -213,7 +222,11 @@ "cordova-plugin-wkwebview-cookies": {}, "cordova-plugin-qrscanner": {}, "cordova-plugin-chooser": {}, - "cordova-plugin-wkuserscript": {} + "cordova-plugin-wkuserscript": {}, + "cordova-plugin-media": { + "KEEP_AVAUDIOSESSION_ALWAYS_ACTIVE": "NO" + }, + "cordova.plugins.diagnostic": {} } }, "main": "desktop/electron.js", @@ -248,14 +261,19 @@ } ], "compression": "maximum", - "electronVersion": "4.2.5", + "electronVersion": "8.0.2", "mac": { "category": "public.app-category.education", "icon": "resources/desktop/icon.icns", "target": "mas", - "bundleVersion": "3.9.2", + "bundleVersion": "3.9.3", "extendInfo": { - "ElectronTeamID": "2NU57U5PAW" + "ElectronTeamID": "2NU57U5PAW", + "NSLocationWhenInUseUsageDescription": "We need your location so you can attach it as part of your submissions.", + "NSLocationAlwaysUsageDescription": "We need your location so you can attach it as part of your submissions.", + "NSCameraUsageDescription": "We need camera access to take pictures so you can attach them as part of your submissions.", + "NSMicrophoneUsageDescription": "We need microphone access to record sounds so you can attach them as part of your submissions.", + "NSPhotoLibraryUsageDescription": "We need photo library access to get pictures from there so you can attach them as part of your submissions." } }, "win": { @@ -274,6 +292,6 @@ } }, "engines": { - "node": "11.x" + "node": ">=11.x" } -} \ No newline at end of file +} diff --git a/scripts/create_langindex.sh b/scripts/create_langindex.sh index d35ba1d74..9a82b64e0 100755 --- a/scripts/create_langindex.sh +++ b/scripts/create_langindex.sh @@ -258,7 +258,10 @@ function parse_file { value=`$exec` guess_file $key "$value" else - if [ ! -z "$findbetter" ]; then + if [ "$found" == 'donottranslate' ]; then + # Do nothing since is not translatable. + continue + elif [ ! -z "$findbetter" ]; then exec="jq -r .\"$key\" $1" value=`$exec` find_better_file "$key" "$value" "$found" diff --git a/scripts/lang_functions.php b/scripts/lang_functions.php index ea94ec3d1..668070a60 100644 --- a/scripts/lang_functions.php +++ b/scripts/lang_functions.php @@ -210,6 +210,18 @@ function build_lang($lang, $keys) { $string = get_translation_strings($langfoldername, $value->file, $override_langfolder); // Apply translations. if (!$string) { + if ($value->file == 'donottranslate') { + // Restore it form the json. + if ($langFile && is_array($langFile) && isset($langFile[$key])) { + $translations[$key] = $langFile[$key]; + } else { + // If not present, do not count it in the total. + $total--; + } + + continue; + } + if (TOTRANSLATE) { echo "\n\t\tTo translate $value->string on $value->file"; } @@ -312,6 +324,10 @@ function detect_lang($lang, $keys) { $string = get_translation_strings($langfoldername, $value->file); // Apply translations. if (!$string) { + // Do not count non translatable in the totals. + if ($value->file == 'donottranslate') { + $total--; + } continue; } diff --git a/scripts/langindex.json b/scripts/langindex.json index fe23eb125..0a3a21d16 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -45,7 +45,6 @@ "addon.block_myoverview.hiddencourses": "block_myoverview", "addon.block_myoverview.inprogress": "block_myoverview", "addon.block_myoverview.lastaccessed": "block_myoverview", - "addon.block_myoverview.morecourses": "block_myoverview", "addon.block_myoverview.nocourses": "block_myoverview", "addon.block_myoverview.past": "block_myoverview", "addon.block_myoverview.pluginname": "block_myoverview", @@ -408,6 +407,7 @@ "addon.mod_assign.submitassignment_help": "assign", "addon.mod_assign.submittedearly": "assign", "addon.mod_assign.submittedlate": "assign", + "addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp", "addon.mod_assign.timemodified": "assign", "addon.mod_assign.timeremaining": "assign", "addon.mod_assign.ungroupedusers": "assign", @@ -463,6 +463,7 @@ "addon.mod_choice.errorgetchoice": "local_moodlemobileapp", "addon.mod_choice.expired": "choice", "addon.mod_choice.full": "choice", + "addon.mod_choice.limita": "choice", "addon.mod_choice.modulenameplural": "choice", "addon.mod_choice.noresultsviewable": "choice", "addon.mod_choice.notopenyet": "choice", @@ -476,6 +477,7 @@ "addon.mod_choice.publishinfonever": "choice", "addon.mod_choice.removemychoice": "choice", "addon.mod_choice.responses": "choice", + "addon.mod_choice.responsesa": "choice", "addon.mod_choice.responsesresultgraphdescription": "local_moodlemobileapp", "addon.mod_choice.responsesresultgraphheader": "choice", "addon.mod_choice.resultsnotsynced": "local_moodlemobileapp", @@ -505,11 +507,13 @@ "addon.mod_data.foundrecords": "data", "addon.mod_data.gettinglocation": "local_moodlemobileapp", "addon.mod_data.latlongboth": "data", + "addon.mod_data.locationnotenabled": "local_moodlemobileapp", "addon.mod_data.locationpermissiondenied": "local_moodlemobileapp", "addon.mod_data.menuchoose": "data", "addon.mod_data.modulenameplural": "data", "addon.mod_data.more": "data", "addon.mod_data.mylocation": "local_moodlemobileapp", + "addon.mod_data.noaccess": "data", "addon.mod_data.nomatch": "data", "addon.mod_data.norecords": "data", "addon.mod_data.notapproved": "data", @@ -1370,6 +1374,8 @@ "core.cannotconnecttrouble": "local_moodlemobileapp", "core.cannotconnectverify": "local_moodlemobileapp", "core.cannotdownloadfiles": "local_moodlemobileapp", + "core.cannotopeninapp": "local_moodlemobileapp", + "core.cannotopeninappdownload": "local_moodlemobileapp", "core.captureaudio": "local_moodlemobileapp", "core.capturedimage": "local_moodlemobileapp", "core.captureimage": "local_moodlemobileapp", @@ -1378,6 +1384,7 @@ "core.choose": "moodle", "core.choosedots": "moodle", "core.clearsearch": "local_moodlemobileapp", + "core.clearstoreddata": "local_moodlemobileapp", "core.clicktohideshow": "moodle", "core.clicktoseefull": "local_moodlemobileapp", "core.close": "repository", @@ -1431,6 +1438,7 @@ "core.course.availablespace": "local_moodlemobileapp", "core.course.cannotdeletewhiledownloading": "local_moodlemobileapp", "core.course.confirmdeletemodulefiles": "local_moodlemobileapp", + "core.course.confirmdeletestoreddata": "local_moodlemobileapp", "core.course.confirmdownload": "local_moodlemobileapp", "core.course.confirmdownloadunknownsize": "local_moodlemobileapp", "core.course.confirmdownloadzerosize": "local_moodlemobileapp", @@ -1522,6 +1530,7 @@ "core.done": "survey", "core.download": "moodle", "core.downloaded": "local_moodlemobileapp", + "core.downloadfile": "moodle", "core.downloading": "local_moodlemobileapp", "core.edit": "moodle", "core.editor.autosavesucceeded": "editor_atto", @@ -1557,6 +1566,7 @@ "core.errorsomedatanotdownloaded": "local_moodlemobileapp", "core.errorsync": "local_moodlemobileapp", "core.errorsyncblocked": "local_moodlemobileapp", + "core.errorurlschemeinvalidscheme": "local_moodlemobileapp", "core.errorurlschemeinvalidsite": "local_moodlemobileapp", "core.explanationdigitalminor": "moodle", "core.favourites": "moodle", @@ -1750,6 +1760,7 @@ "core.login.erroraccesscontrolalloworigin": "local_moodlemobileapp", "core.login.errordeletesite": "local_moodlemobileapp", "core.login.errorexampleurl": "local_moodlemobileapp", + "core.login.errorqrnoscheme": "local_moodlemobileapp", "core.login.errorupdatesite": "local_moodlemobileapp", "core.login.faqcannotconnectanswer": "local_moodlemobileapp", "core.login.faqcannotconnectquestion": "local_moodlemobileapp", @@ -1826,7 +1837,9 @@ "core.login.selectacountry": "moodle", "core.login.selectsite": "local_moodlemobileapp", "core.login.signupplugindisabled": "local_moodlemobileapp", + "core.login.signuprequiredfieldnotsupported": "local_moodlemobileapp", "core.login.siteaddress": "local_moodlemobileapp", + "core.login.siteaddressplaceholder": "donottranslate", "core.login.sitehasredirect": "local_moodlemobileapp", "core.login.siteinmaintenance": "local_moodlemobileapp", "core.login.sitepolicynotagreederror": "local_moodlemobileapp", @@ -1848,6 +1861,7 @@ "core.mainmenu.help": "moodle", "core.mainmenu.logout": "moodle", "core.mainmenu.website": "local_moodlemobileapp", + "core.maxfilesize": "moodle", "core.maxsizeandattachments": "moodle", "core.min": "moodle", "core.mins": "moodle", @@ -1899,6 +1913,7 @@ "core.noresults": "moodle", "core.noselection": "form", "core.notapplicable": "local_moodlemobileapp", + "core.notavailable": "moodle", "core.notenrolledprofile": "moodle", "core.notice": "moodle", "core.notingroup": "moodle", @@ -1909,6 +1924,7 @@ "core.offline": "message", "core.ok": "moodle", "core.online": "message", + "core.openfile": "local_moodlemobileapp", "core.openfullimage": "local_moodlemobileapp", "core.openinbrowser": "local_moodlemobileapp", "core.openmodinbrowser": "local_moodlemobileapp", @@ -1929,8 +1945,8 @@ "core.question.certainty": "qbehaviour_deferredcbm", "core.question.complete": "question", "core.question.correct": "question", - "core.question.errorattachmentsnotsupported": "local_moodlemobileapp", - "core.question.errorinlinefilesnotsupported": "local_moodlemobileapp", + "core.question.errorattachmentsnotsupportedinsite": "local_moodlemobileapp", + "core.question.errorembeddedfilesnotsupportedinsite": "local_moodlemobileapp", "core.question.errorquestionnotsupported": "local_moodlemobileapp", "core.question.feedback": "question", "core.question.howtodraganddrop": "local_moodlemobileapp", @@ -1981,6 +1997,7 @@ "core.settings.about": "local_moodlemobileapp", "core.settings.appsettings": "local_moodlemobileapp", "core.settings.appversion": "local_moodlemobileapp", + "core.settings.cannotsyncloggedout": "local_moodlemobileapp", "core.settings.cannotsyncoffline": "local_moodlemobileapp", "core.settings.cannotsyncwithoutwifi": "local_moodlemobileapp", "core.settings.colorscheme": "local_moodlemobileapp", diff --git a/scripts/test_e2e.sh b/scripts/test_e2e.sh index 22edff551..50908f91a 100755 --- a/scripts/test_e2e.sh +++ b/scripts/test_e2e.sh @@ -17,16 +17,7 @@ export MOODLE_DOCKER_APP_PATH=$basedir print_title "Preparing dependencies" git clone --branch master --depth 1 git://github.com/moodle/moodle $HOME/moodle git clone --branch master --depth 1 git://github.com/moodlehq/moodle-local_moodlemobileapp $HOME/moodle/local/moodlemobileapp -# git clone --branch master --depth 1 git://github.com/moodlehq/moodle-docker $HOME/moodle-docker - -# TODO replace with commented line above once https://github.com/moodlehq/moodle-docker/pull/126 is merged -mkdir $HOME/moodle-docker -cd $HOME/moodle-docker -git init -git remote add origin git://github.com/moodlehq/moodle-docker -git fetch --depth 1 origin c604d5f9792c72fb9d83f6fec1f4b1defd778e9a -git checkout FETCH_HEAD -cd - +git clone --branch master --depth 1 git://github.com/moodlehq/moodle-docker $HOME/moodle-docker cp $HOME/moodle-docker/config.docker-template.php $HOME/moodle/config.php @@ -50,7 +41,7 @@ print_title "Running e2e tests" # Run tests for tags in "$@" do - $dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags=\"$tags\"" + $dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags=\"$tags\" --auto-rerun" notify_on_error_exit "Some e2e tests are failing, please review" done diff --git a/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html index 4b7495bfa..1cb51f1e2 100644 --- a/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -42,7 +42,7 @@
- + diff --git a/src/addon/block/myoverview/components/myoverview/myoverview.ts b/src/addon/block/myoverview/components/myoverview/myoverview.ts index 074b176b9..7fd24084c 100644 --- a/src/addon/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addon/block/myoverview/components/myoverview/myoverview.ts @@ -77,6 +77,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem downloadCourseEnabled: boolean; downloadCoursesEnabled: boolean; + protected FILTER_PRIORITY = ['all', 'allincludinghidden', 'inprogress', 'future', 'past', 'favourite', 'hidden', 'custom']; protected prefetchIconsInitialized = false; protected isDestroyed; protected coursesObserver; @@ -202,9 +203,6 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.initCourseFilters(courses); - this.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 || - typeof courses[0].enddate != 'undefined'); - this.courses.filter = ''; this.showFilter = false; @@ -250,10 +248,15 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.showSelectorFilter = Object.keys(this.showFilters).some((key) => { return this.showFilters[key] == 'show'; }); + + if (!this.showSelectorFilter) { + // All filters disabled, display all the courses. + this.showFilters.all = 'show'; + } } - if (!this.showSelectorFilter || (this.selectedFilter === 'inprogress' && this.showFilters.inprogress == 'disabled')) { - // No selector, or the default option is disabled, show all. + if (!this.showSelectorFilter) { + // No selector, display all the courses. this.selectedFilter = 'all'; } this.setCourseFilter(this.selectedFilter); @@ -388,7 +391,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem if (this.showFilters[filter] == 'show') { this.filteredCourses = this.courses[filter]; } else { - const activeFilter = Object.keys(this.showFilters).find((name) => { + const activeFilter = this.FILTER_PRIORITY.find((name) => { return this.showFilters[name] == 'show'; }); diff --git a/src/addon/block/myoverview/lang/en.json b/src/addon/block/myoverview/lang/en.json index 9d38164d8..7bca82636 100644 --- a/src/addon/block/myoverview/lang/en.json +++ b/src/addon/block/myoverview/lang/en.json @@ -6,7 +6,6 @@ "hiddencourses": "Removed from view", "inprogress": "In progress", "lastaccessed": "Last accessed", - "morecourses": "More courses", "nocourses": "No courses", "past": "Past", "pluginname": "Course overview", diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index a48f1d51f..1f36db633 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -64,12 +64,12 @@ - - - - - + + + + + diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 5d40bbd31..a37528e3e 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -1606,61 +1606,55 @@ export class AddonCalendarProvider { * @param siteId Site ID the event belongs to. If not defined, use current site. * @return Promise resolved when the notification is scheduled. */ - protected scheduleEventNotification(event: AddonCalendarAnyEvent, reminderId: number, time: number, siteId?: string) + protected async scheduleEventNotification(event: AddonCalendarAnyEvent, reminderId: number, time: number, siteId?: string) : Promise { - if (this.localNotificationsProvider.isAvailable()) { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + if (!this.localNotificationsProvider.isAvailable()) { + return; + } - if (time === 0) { - // Cancel if it was scheduled. + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (time === 0) { + // Cancel if it was scheduled. + return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); + } + + if (time == -1) { + // If time is -1, get event default time to calculate the notification time. + time = await this.getDefaultNotificationTime(siteId); + + if (time == 0) { + // Default notification time is disabled, do not show. return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); } - let promise; - if (time == -1) { - // If time is -1, get event default time to calculate the notification time. - promise = this.getDefaultNotificationTime(siteId).then((time) => { - if (time == 0) { - // Default notification time is disabled, do not show. - return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); - } - - return event.timestart - (time * 60); - }); - } else { - promise = Promise.resolve(time); - } - - return promise.then((time) => { - time = time * 1000; - - if (time <= new Date().getTime()) { - // This reminder is over, don't schedule. Cancel if it was scheduled. - return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); - } - - const notification: ILocalNotification = { - id: reminderId, - title: event.name, - text: this.timeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true), - icon: 'file://assets/img/icons/calendar.png', - trigger: { - at: new Date(time) - }, - data: { - eventid: event.id, - reminderid: reminderId, - siteid: siteId - } - }; - - return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId); - }); - - } else { - return Promise.resolve(); + time = event.timestart - (time * 60); } + + time = time * 1000; + + if (time <= Date.now()) { + // This reminder is over, don't schedule. Cancel if it was scheduled. + return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); + } + + const notification: ILocalNotification = { + id: reminderId, + title: event.name, + text: this.timeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true), + icon: 'file://assets/img/icons/calendar.png', + trigger: { + at: new Date(time) + }, + data: { + eventid: event.id, + reminderid: reminderId, + siteid: siteId + } + }; + + return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId); } /** diff --git a/src/addon/competency/providers/competency.ts b/src/addon/competency/providers/competency.ts index 7d2704da9..ca1eadb8b 100644 --- a/src/addon/competency/providers/competency.ts +++ b/src/addon/competency/providers/competency.ts @@ -62,6 +62,39 @@ export class AddonCompetencyProvider { }); } + /** + * Returns whether current user can see another user competencies in a course. + * + * @param courseId Course ID. + * @param userId User ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether the user can view the competencies. + */ + canViewUserCompetenciesInCourse(courseId: number, userId?: number, siteId?: string): Promise { + if (!this.sitesProvider.isLoggedIn()) { + return Promise.resolve(false); + } + + return this.getCourseCompetenciesPage(courseId, siteId).then((response) => { + if (!response.competencies.length) { + // No competencies. + return false; + } + + if (!userId || userId == this.sitesProvider.getCurrentSiteUserId()) { + // Current user. + return true; + } + + // Check if current user can view any competency of the user. + return this.getCompetencyInCourse(courseId, response.competencies[0].competency.id, userId, siteId).then(() => { + return true; + }); + }).catch(() => { + return false; + }); + } + /** * Get cache key for user learning plans data WS calls. * @@ -333,7 +366,7 @@ export class AddonCompetencyProvider { } /** - * Get all competencies in a course. + * Get all competencies in a course for a certain user. * * @param courseId ID of the course. * @param userId ID of the user. @@ -344,6 +377,39 @@ export class AddonCompetencyProvider { getCourseCompetencies(courseId: number, userId?: number, siteId?: string, ignoreCache?: boolean) : Promise { + return this.getCourseCompetenciesPage(courseId, siteId, ignoreCache).then((response) => { + + if (!userId || userId == this.sitesProvider.getCurrentSiteUserId()) { + return response; + } + + let promises: Promise[]; + + promises = response.competencies.map((competency) => + this.getCompetencyInCourse(courseId, competency.competency.id, userId, siteId) + ); + + return Promise.all(promises).then((responses: AddonCompetencyUserCompetencySummaryInCourse[]) => { + responses.forEach((resp, index) => { + response.competencies[index].usercompetencycourse = resp.usercompetencysummary.usercompetencycourse; + }); + + return response; + }); + }); + } + + /** + * Get all competencies in a course. + * + * @param courseId ID of the course. + * @param siteId Site ID. If not defined, current site. + * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @return Promise to be resolved when the course competencies are retrieved. + */ + getCourseCompetenciesPage(courseId: number, siteId?: string, ignoreCache?: boolean) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { this.logger.debug('Get course competencies for course ' + courseId); @@ -370,26 +436,6 @@ export class AddonCompetencyProvider { return Promise.reject(null); }); - - }).then((response) => { - - if (!userId || userId == this.sitesProvider.getCurrentSiteUserId()) { - return response; - } - - let promises: Promise[]; - - promises = response.competencies.map((competency) => - this.getCompetencyInCourse(courseId, competency.competency.id, userId, siteId) - ); - - return Promise.all(promises).then((responses: AddonCompetencyUserCompetencySummaryInCourse[]) => { - responses.forEach((resp, index) => { - response.competencies[index].usercompetencycourse = resp.usercompetencysummary.usercompetencycourse; - }); - - return response; - }); }); } diff --git a/src/addon/competency/providers/user-handler.ts b/src/addon/competency/providers/user-handler.ts index b2c07ebc0..216909eda 100644 --- a/src/addon/competency/providers/user-handler.ts +++ b/src/addon/competency/providers/user-handler.ts @@ -71,15 +71,10 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler { return this.participantsNavEnabledCache[cacheKey]; } - return this.competencyProvider.getCourseCompetencies(courseId, user.id).then((response) => { - const enabled = response.competencies.length > 0; + return this.competencyProvider.canViewUserCompetenciesInCourse(courseId, user.id).then((enabled) => { this.participantsNavEnabledCache[cacheKey] = enabled; return enabled; - }).catch((message) => { - this.participantsNavEnabledCache[cacheKey] = false; - - return false; }); } else { // Link on a user site profile. diff --git a/src/addon/messageoutput/airnotifier/airnotifier.module.ts b/src/addon/messageoutput/airnotifier/airnotifier.module.ts index b8a3b3ca6..78a7fcff0 100644 --- a/src/addon/messageoutput/airnotifier/airnotifier.module.ts +++ b/src/addon/messageoutput/airnotifier/airnotifier.module.ts @@ -13,6 +13,7 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; import { AddonMessageOutputAirnotifierProvider } from './providers/airnotifier'; import { AddonMessageOutputAirnotifierHandler } from './providers/handler'; @@ -28,7 +29,13 @@ import { AddonMessageOutputAirnotifierHandler } from './providers/handler'; ] }) export class AddonMessageOutputAirnotifierModule { - constructor(messageOutputDelegate: AddonMessageOutputDelegate, airnotifierHandler: AddonMessageOutputAirnotifierHandler) { + constructor(messageOutputDelegate: AddonMessageOutputDelegate, airnotifierHandler: AddonMessageOutputAirnotifierHandler, + eventsProvider: CoreEventsProvider, airnotifierProvider: AddonMessageOutputAirnotifierProvider) { messageOutputDelegate.registerHandler(airnotifierHandler); + + eventsProvider.on(CoreEventsProvider.DEVICE_REGISTERED_IN_MOODLE, async (data) => { + // Get user devices to make Moodle send the devices data to Airnotifier. + airnotifierProvider.getUserDevices(true, data.siteId); + }); } } diff --git a/src/addon/messageoutput/airnotifier/pages/devices/devices.html b/src/addon/messageoutput/airnotifier/pages/devices/devices.html index 82f688482..24434141b 100644 --- a/src/addon/messageoutput/airnotifier/pages/devices/devices.html +++ b/src/addon/messageoutput/airnotifier/pages/devices/devices.html @@ -11,7 +11,7 @@ - {{ device.model }} + {{ device.name }} {{ device.model }} {{ device.platform }} {{ device.version }} ({{ 'core.currentdevice' | translate }}) diff --git a/src/addon/messageoutput/airnotifier/providers/airnotifier.ts b/src/addon/messageoutput/airnotifier/providers/airnotifier.ts index 01d429774..f29c4ba48 100644 --- a/src/addon/messageoutput/airnotifier/providers/airnotifier.ts +++ b/src/addon/messageoutput/airnotifier/providers/airnotifier.ts @@ -74,10 +74,11 @@ export class AddonMessageOutputAirnotifierProvider { /** * Get user devices. * + * @param ignoreCache Whether to ignore cache. * @param siteId Site ID. If not defined, use current site. * @return Promise resolved with the devices. */ - getUserDevices(siteId?: string): Promise { + getUserDevices(ignoreCache?: boolean, siteId?: string): Promise { this.logger.debug('Get user devices'); return this.sitesProvider.getSite(siteId).then((site) => { @@ -89,6 +90,11 @@ export class AddonMessageOutputAirnotifierProvider { updateFrequency: CoreSite.FREQUENCY_RARELY }; + if (ignoreCache) { + preSets['getFromCache'] = false; + preSets['emergencyCache'] = false; + } + return site.read('message_airnotifier_get_user_devices', data, preSets) .then((data: AddonMessageOutputAirnotifierGetUserDevicesResult) => { return data.devices; diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index 043376f1d..11a4046a1 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -581,35 +581,38 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param offset Offset for message list. * @return Promise resolved with the list of messages. */ - protected getConversationMessages(pagesToLoad: number, offset: number = 0) + protected async getConversationMessages(pagesToLoad: number, offset: number = 0) : Promise { const excludePending = offset > 0; - return this.messagesProvider.getConversationMessages(this.conversationId, excludePending, offset).then((result) => { - pagesToLoad--; - - // Treat members. Don't use CoreUtilsProvider.arrayToObject because we don't want to override the existing object. - if (result.members) { - result.members.forEach((member) => { - this.members[member.id] = member; - }); - } - - if (pagesToLoad > 0 && result.canLoadMore) { - offset += AddonMessagesProvider.LIMIT_MESSAGES; - - // Get more messages. - return this.getConversationMessages(pagesToLoad, offset).then((nextMessages) => { - return result.messages.concat(nextMessages); - }); - } else { - // No more messages to load, return them. - this.canLoadMore = result.canLoadMore; - - return result.messages; - } + const result = await this.messagesProvider.getConversationMessages(this.conversationId, { + excludePending: excludePending, + limitFrom: offset, }); + + pagesToLoad--; + + // Treat members. Don't use CoreUtilsProvider.arrayToObject because we don't want to override the existing object. + if (result.members) { + result.members.forEach((member) => { + this.members[member.id] = member; + }); + } + + if (pagesToLoad > 0 && result.canLoadMore) { + offset += AddonMessagesProvider.LIMIT_MESSAGES; + + // Get more messages. + const nextMessages = await this.getConversationMessages(pagesToLoad, offset); + + return result.messages.concat(nextMessages); + } else { + // No more messages to load, return them. + this.canLoadMore = result.canLoadMore; + + return result.messages; + } } /** diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index 0288cb13d..6033ad8fd 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -22,7 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper'; import { CoreEventsProvider } from '@providers/events'; -import { CoreSite } from '@classes/site'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreWSExternalWarning } from '@providers/ws'; /** @@ -398,7 +398,7 @@ export class AddonMessagesProvider { * @param userId The other person with whom the current user is having the discussion. * @return Cache key. */ - protected getCacheKeyForDiscussion(userId: number): string { + getCacheKeyForDiscussion(userId: number): string { return this.ROOT_CACHE_KEY + 'discussion:' + userId; } @@ -889,93 +889,92 @@ export class AddonMessagesProvider { * Get a conversation by the conversation ID. * * @param conversationId Conversation ID to fetch. - * @param excludePending True to exclude messages pending to be sent. - * @param limitFrom Offset for messages list. - * @param limitTo Limit of messages. - * @param newestFirst Whether to order messages by newest first. - * @param timeFrom The timestamp from which the messages were created. - * @param siteId Site ID. If not defined, use current site. - * @param userId User ID. If not defined, current user in the site. + * @param options Options. * @return Promise resolved with the response. * @since 3.6 */ - getConversationMessages(conversationId: number, excludePending: boolean, limitFrom: number = 0, limitTo?: number, - newestFirst: boolean = true, timeFrom: number = 0, siteId?: string, userId?: number) + async getConversationMessages(conversationId: number, options?: AddonMessagesGetConversationMessagesOptions) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + options = options || {}; - if (typeof limitTo == 'undefined' || limitTo === null) { - limitTo = this.LIMIT_MESSAGES; + const site = await this.sitesProvider.getSite(options.siteId); + + options.userId = options.userId || site.getUserId(); + options.limitFrom = options.limitFrom || 0; + options.limitTo = options.limitTo === undefined || options.limitTo === null ? this.LIMIT_MESSAGES : options.limitTo; + options.timeFrom = options.timeFrom || 0; + options.newestFirst = options.newestFirst === undefined || options.newestFirst === null ? true : options.newestFirst; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCacheKeyForConversationMessages(options.userId, conversationId), + }; + const params = { + currentuserid: options.userId, + convid: conversationId, + limitfrom: options.limitFrom, + limitnum: options.limitTo < 1 ? options.limitTo : options.limitTo + 1, // If there's a limit, get 1 more than requested. + newest: options.newestFirst ? 1 : 0, + timefrom: options.timeFrom, + }; + + if (options.limitFrom > 0) { + // Do not use cache when retrieving older messages. + // This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded. + preSets.getFromCache = false; + preSets.saveToCache = false; + preSets.emergencyCache = false; + } else if (options.forceCache) { + preSets.omitExpires = true; + } else if (options.ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + const result: AddonMessagesGetConversationMessagesResult = + await site.read('core_message_get_conversation_messages', params, preSets); + + if (options.limitTo < 1) { + result.canLoadMore = false; + result.messages = result.messages; + } else { + result.canLoadMore = result.messages.length > options.limitTo; + result.messages = result.messages.slice(0, options.limitTo); + } + + let lastReceived; + + result.messages.forEach((message) => { + // Convert time to milliseconds. + message.timecreated = message.timecreated ? message.timecreated * 1000 : 0; + + if (!lastReceived && message.useridfrom != options.userId) { + lastReceived = message; } - - const preSets = { - cacheKey: this.getCacheKeyForConversationMessages(userId, conversationId) - }, - params: any = { - currentuserid: userId, - convid: conversationId, - limitfrom: limitFrom, - limitnum: limitTo < 1 ? limitTo : limitTo + 1, // If there is a limit, get 1 more than requested. - newest: newestFirst ? 1 : 0, - timefrom: timeFrom - }; - - if (limitFrom > 0) { - // Do not use cache when retrieving older messages. - // This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded. - preSets['getFromCache'] = false; - preSets['saveToCache'] = false; - preSets['emergencyCache'] = false; - } - - return site.read('core_message_get_conversation_messages', params, preSets) - .then((result: AddonMessagesGetConversationMessagesResult) => { - - if (limitTo < 1) { - result.canLoadMore = false; - result.messages = result.messages; - } else { - result.canLoadMore = result.messages.length > limitTo; - result.messages = result.messages.slice(0, limitTo); - } - - let lastReceived; - - result.messages.forEach((message) => { - // Convert time to milliseconds. - message.timecreated = message.timecreated ? message.timecreated * 1000 : 0; - - if (!lastReceived && message.useridfrom != userId) { - lastReceived = message; - } - }); - - if (this.appProvider.isDesktop() && limitFrom === 0 && lastReceived) { - // Store the last received message (we cannot know if it's unread or not). Don't block the user for this. - this.storeLastReceivedMessageIfNeeded(conversationId, lastReceived, site.getId()); - } - - if (excludePending) { - // No need to get offline messages, return the ones we have. - return result; - } - - // Get offline messages. - return this.messagesOffline.getConversationMessages(conversationId).then((offlineMessages) => { - // Mark offline messages as pending. - offlineMessages.forEach((message) => { - message.pending = true; - message.useridfrom = userId; - }); - - result.messages = result.messages.concat(offlineMessages); - - return result; - }); - }); }); + + if (this.appProvider.isDesktop() && options.limitFrom === 0 && lastReceived) { + // Store the last received message (we cannot know if it's unread or not). Don't block the user for this. + this.storeLastReceivedMessageIfNeeded(conversationId, lastReceived, site.getId()); + } + + if (options.excludePending) { + // No need to get offline messages, return the ones we have. + return result; + } + + // Get offline messages. + const offlineMessages = await this.messagesOffline.getConversationMessages(conversationId); + + // Mark offline messages as pending. + offlineMessages.forEach((message) => { + message.pending = true; + message.useridfrom = options.userId; + }); + + result.messages = result.messages.concat(offlineMessages); + + return result; } /** @@ -1412,7 +1411,7 @@ export class AddonMessagesProvider { * @param siteId Site ID. If not defined, use current site. * @return Promise resolved with the data. */ - protected getRecentMessages(params: any, preSets: any, limitFromUnread: number = 0, limitFromRead: number = 0, + getRecentMessages(params: any, preSets: any, limitFromUnread: number = 0, limitFromRead: number = 0, toDisplay: boolean = true, siteId?: string): Promise { limitFromUnread = limitFromUnread || 0; limitFromRead = limitFromRead || 0; @@ -1962,7 +1961,8 @@ export class AddonMessagesProvider { * @since 3.2 */ isMarkAllMessagesReadEnabled(): boolean { - return this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_messages_as_read'); + return this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_conversation_messages_as_read') || + this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_messages_as_read'); } /** @@ -2787,6 +2787,21 @@ export class AddonMessagesProvider { } } +/** + * Options to pass to getConversationMessages. + */ +export type AddonMessagesGetConversationMessagesOptions = { + excludePending?: boolean; // True to exclude messages pending to be sent. + limitFrom?: number; // Offset for messages list. Defaults to 0. + limitTo?: number; // Limit of messages. + newestFirst?: boolean; // Whether to order messages by newest first. + timeFrom?: number; // The timestamp from which the messages were created (in seconds). Defaults to 0. + siteId?: string; // Site ID. If not defined, use current site. + userId?: number; // User ID. If not defined, current user in the site. + forceCache?: boolean; // True if it should return cached data. Has priority over ignoreCache. + ignoreCache?: boolean; // True if it should ignore cached data (it will always fail in offline or server down). +}; + /** * Conversation. */ diff --git a/src/addon/messages/providers/settings-handler.ts b/src/addon/messages/providers/settings-handler.ts index b3a4b62df..cbee96b44 100644 --- a/src/addon/messages/providers/settings-handler.ts +++ b/src/addon/messages/providers/settings-handler.ts @@ -32,8 +32,10 @@ export class AddonMessagesSettingsHandler implements CoreSettingsHandler { * * @return Whether or not the handler is enabled on a site level. */ - isEnabled(): boolean | Promise { - return this.messagesProvider.isMessagePreferencesEnabled(); + async isEnabled(): Promise { + const messagingEnabled = await this.messagesProvider.isPluginEnabled(); + + return messagingEnabled && this.messagesProvider.isMessagePreferencesEnabled(); } /** diff --git a/src/addon/messages/providers/sync.ts b/src/addon/messages/providers/sync.ts index 09e024884..fe7e83e71 100644 --- a/src/addon/messages/providers/sync.ts +++ b/src/addon/messages/providers/sync.ts @@ -26,6 +26,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { TranslateService } from '@ngx-translate/core'; import { CoreSyncProvider } from '@providers/sync'; +import { CoreConstants } from '@core/constants'; /** * Service to sync messages. @@ -134,112 +135,165 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { * * @param conversationId Conversation ID. * @param userId User ID talking to (if no conversation ID). - * @return Promise resolved if sync is successful, rejected otherwise. + * @param siteId Site ID. + * @return Promise resolved with the list of warnings if sync is successful, rejected otherwise. */ - syncDiscussion(conversationId: number, userId: number, siteId?: string): Promise { + syncDiscussion(conversationId: number, userId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - const syncId = this.getSyncId(conversationId, userId), - groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); + const syncId = this.getSyncId(conversationId, userId); if (this.isSyncing(syncId, siteId)) { // There's already a sync ongoing for this conversation, return the promise. return this.getOngoingSync(syncId, siteId); } - const warnings = []; + return this.addOngoingSync(syncId, this.performSyncDiscussion(conversationId, userId, siteId), siteId); + } + + /** + * Perform the synchronization of a discussion. + * + * @param conversationId Conversation ID. + * @param userId User ID talking to (if no conversation ID). + * @param siteId Site ID. + * @return Promise resolved with the list of warnings if sync is successful, rejected otherwise. + */ + protected async performSyncDiscussion(conversationId: number, userId: number, siteId: string): Promise { + const groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); + let messages: any[]; + const errors = []; + const warnings: string[] = []; if (conversationId) { this.logger.debug(`Try to sync conversation '${conversationId}'`); + messages = await this.messagesOffline.getConversationMessages(conversationId, siteId); } else { this.logger.debug(`Try to sync discussion with user '${userId}'`); + messages = await this.messagesOffline.getMessages(userId, siteId); } - // Get offline messages to be sent. - let syncPromise; + if (!messages.length) { + // Nothing to sync. + return []; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. Mark messages as device offline. + this.messagesOffline.setMessagesDeviceOffline(messages, true); - if (conversationId) { - syncPromise = this.messagesOffline.getConversationMessages(conversationId, siteId); - } else { - syncPromise = this.messagesOffline.getMessages(userId, siteId); + return Promise.reject(null); } - syncPromise = syncPromise.then((messages) => { - if (!messages.length) { - // Nothing to sync. - return []; - } else if (!this.appProvider.isOnline()) { - // Cannot sync in offline. Mark messages as device offline. - this.messagesOffline.setMessagesDeviceOffline(messages, true); + // Order message by timecreated. + messages = this.messagesProvider.sortMessages(messages); - return Promise.reject(null); - } + // Get messages sent by the user after the first offline message was sent. + // We subtract some time because the message could've been saved in server before it was in the app. + const timeFrom = Math.floor((messages[0].timecreated - CoreConstants.WS_TIMEOUT - 1000) / 1000); + const onlineMessages = await this.getMessagesSentAfter(timeFrom, conversationId, userId, siteId); - let promise: Promise = Promise.resolve(); - const errors = []; + // Send the messages. Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed. + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + const textFieldName = conversationId ? 'text' : 'smallmessage'; + const wrappedText = message[textFieldName][0] != '<' ? '

' + message[textFieldName] + '

' : message[textFieldName]; - // Order message by timecreated. - messages = this.messagesProvider.sortMessages(messages); - - // Send the messages. - // Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed. - messages.forEach((message, index) => { - // Chain message sending. If 1 message fails to be sent we'll stop sending. - promise = promise.then(() => { - let subPromise; - - if (conversationId) { - subPromise = this.messagesProvider.sendMessageToConversationOnline(conversationId, message.text, siteId); - } else { - subPromise = this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId); + try { + if (onlineMessages.indexOf(wrappedText) != -1) { + // Message already sent, ignore it to prevent duplicates. + } else if (conversationId) { + await this.messagesProvider.sendMessageToConversationOnline(conversationId, message.text, siteId); + } else { + await this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId); + } + } catch (error) { + if (!this.utils.isWebServiceError(error)) { + // Error sending, stop execution. + if (this.appProvider.isOnline()) { + // App is online, unmark deviceoffline if marked. + this.messagesOffline.setMessagesDeviceOffline(messages, false); } - return subPromise.catch((error) => { - if (this.utils.isWebServiceError(error)) { - // Error returned by WS. Store the error to show a warning but keep sending messages. - if (errors.indexOf(error) == -1) { - errors.push(error); - } + throw error; + } - return; - } + // Error returned by WS. Store the error to show a warning but keep sending messages. + if (errors.indexOf(error) == -1) { + errors.push(error); + } + } - // Error sending, stop execution. - if (this.appProvider.isOnline()) { - // App is online, unmark deviceoffline if marked. - this.messagesOffline.setMessagesDeviceOffline(messages, false); - } + // Message was sent, delete it from local DB. + if (conversationId) { + await this.messagesOffline.deleteConversationMessage(conversationId, message.text, message.timecreated, siteId); + } else { + await this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); + } - return Promise.reject(error); - }).then(() => { - // Message was sent, delete it from local DB. - if (conversationId) { - return this.messagesOffline.deleteConversationMessage(conversationId, message.text, - message.timecreated, siteId); - } else { - return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); - } - }).then(() => { - // In some Moodle versions, wait 1 second to make sure timecreated is different. - // This is because there was a bug where messages with the same timecreated had a wrong order. - if (!groupMessagingEnabled && index < messages.length - 1) { - return new Promise((resolve, reject): any => { - setTimeout(resolve, 1000); - }); - } - }); + // In some Moodle versions, wait 1 second to make sure timecreated is different. + // This is because there was a bug where messages with the same timecreated had a wrong order. + if (!groupMessagingEnabled && i < messages.length - 1) { + await this.utils.wait(1000); + } + } + + await this.handleSyncErrors(conversationId, userId, errors, warnings); + + // All done, return the warnings. + return warnings; + } + + /** + * Get messages sent by current user after a certain time. + * + * @param time Time in seconds. + * @param conversationId Conversation ID. + * @param userId User ID talking to (if no conversation ID). + * @param siteId Site ID. + * @return Promise resolved with the messages texts. + */ + protected async getMessagesSentAfter(time: number, conversationId: number, userId: number, siteId: string): Promise { + const site = await this.sitesProvider.getSite(siteId); + + const siteCurrentUserId = site.getUserId(); + + if (conversationId) { + try { + const result = await this.messagesProvider.getConversationMessages(conversationId, { + excludePending: true, + ignoreCache: true, + timeFrom: time, }); - }); - return promise; - }).then((errors) => { - return this.handleSyncErrors(conversationId, userId, errors, warnings); - }).then(() => { - // All done, return the warnings. - return warnings; - }); + const sentMessages = result.messages.filter((message) => message.useridfrom == siteCurrentUserId); - return this.addOngoingSync(syncId, syncPromise, siteId); + return sentMessages.map((message) => message.text); + } catch (error) { + if (error && error.errorcode == 'invalidresponse') { + // There's a bug in Moodle that causes this error if there are no new messages. Return empty array. + return []; + } + + throw error; + } + } else { + + const params = { + useridto: userId, + useridfrom: siteCurrentUserId, + limitnum: AddonMessagesProvider.LIMIT_MESSAGES, + }; + const preSets = { + cacheKey: this.messagesProvider.getCacheKeyForDiscussion(userId), + ignoreCache: true, + }; + + const messages = await this.messagesProvider.getRecentMessages(params, preSets, 0, 0, false, siteId); + + time = time * 1000; // Convert to milliseconds. + const messagesAfterTime = messages.filter((message) => message.timecreated >= time); + + return messagesAfterTime.map((message) => message.text); + } } /** @@ -251,7 +305,7 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { * @param warnings Array where to place the warnings. * @return Promise resolved when done. */ - protected handleSyncErrors(conversationId: number, userId: number, errors: any[], warnings: any[]): Promise { + protected handleSyncErrors(conversationId: number, userId: number, errors: any[], warnings: string[]): Promise { if (errors && errors.length) { if (conversationId) { diff --git a/src/addon/mod/assign/components/index/addon-mod-assign-index.html b/src/addon/mod/assign/components/index/addon-mod-assign-index.html index ee087a9df..18284862e 100644 --- a/src/addon/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addon/mod/assign/components/index/addon-mod-assign-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts index b47535c64..6f872d3c6 100644 --- a/src/addon/mod/assign/components/index/index.ts +++ b/src/addon/mod/assign/components/index/index.ts @@ -175,7 +175,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo this.hasOffline = hasOffline; // Get assignment submissions. - return this.assignProvider.getSubmissions(this.assign.id).then((data) => { + return this.assignProvider.getSubmissions(this.assign.id, {cmId: this.module.id}).then((data) => { const time = this.timeUtils.timestamp(); this.canViewAllSubmissions = data.canviewsubmissions; @@ -217,7 +217,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo } // Check if the user can view their own submission. - return this.assignProvider.getSubmissionStatus(this.assign.id).then(() => { + return this.assignProvider.getSubmissionStatus(this.assign.id, {cmId: this.module.id}).then(() => { this.canViewOwnSubmission = true; }).catch((error) => { this.canViewOwnSubmission = false; @@ -241,7 +241,10 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo setGroup(groupId: number): Promise { this.group = groupId; - return this.assignProvider.getSubmissionStatus(this.assign.id, undefined, this.group).then((response) => { + return this.assignProvider.getSubmissionStatus(this.assign.id, { + groupId: this.group, + cmId: this.module.id, + }).then((response) => { this.summary = response.gradingsummary; if (typeof this.summary.warnofungroupedusers == 'boolean' && this.summary.warnofungroupedusers) { this.summary.warnofungroupedusers = 'ungroupedusers'; @@ -299,7 +302,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo */ protected hasSyncSucceed(result: any): boolean { if (result.updated) { - this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); + this.submissionComponent && this.submissionComponent.invalidateAndRefresh(false); } return result.updated; @@ -324,7 +327,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo } return Promise.all(promises).finally(() => { - this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); + this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); }); } diff --git a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html index e4cb512e5..2164fd42d 100644 --- a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html @@ -20,7 +20,7 @@
- + diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index 6bc9c7282..92cf4167b 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -16,7 +16,7 @@ import { Component, Input, OnInit, OnDestroy, ViewChild, Optional, ViewChildren, import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; -import { CoreEventsProvider } from '@providers/events'; +import { CoreEventsProvider, CoreEventObserver } from '@providers/events'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreLangProvider } from '@providers/lang'; import { CoreSitesProvider } from '@providers/sites'; @@ -35,7 +35,9 @@ import { } from '../../providers/assign'; import { AddonModAssignHelperProvider } from '../../providers/helper'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; +import { AddonModAssignSync, AddonModAssignSyncProvider } from '../../providers/assign-sync'; import { CoreTabsComponent } from '@components/tabs/tabs'; +import { CoreTabComponent } from '@components/tabs/tab'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; @@ -107,6 +109,8 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { protected submissionStatusAvailable: boolean; // Whether we were able to retrieve the submission status. protected originalGrades: any = {}; // Object with the original grade data, to check for changes. protected isDestroyed: boolean; // Whether the component has been destroyed. + protected syncObserver: CoreEventObserver; + protected hasOfflineGrade = false; constructor(protected navCtrl: NavController, protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected timeUtils: CoreTimeUtilsProvider, @@ -129,7 +133,29 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { this.selectedTab = this.showGrade && this.showGrade !== 'false' ? 1 : 0; this.isSubmittedForGrading = !!this.submitId; - this.loadData(); + this.loadData(true); + + // Refresh data if this assign is synchronized and it's grading. + const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED]; + + this.syncObserver = this.eventsProvider.onMultiple(events, async (data) => { + // Check that user is grading and this grade wasn't blocked when sync was performed. + if (!this.loaded || !this.isGrading || data.gradesBlocked.indexOf(this.submitId) != -1) { + return; + } + + if (data.context == 'submission' && data.submitId == this.submitId) { + // Manual sync triggered by this same submission, ignore it. + return; + } + + // Don't refresh if the user has modified some data. + const hasDataToSave = await this.hasDataToSave(); + + if (!hasDataToSave) { + this.invalidateAndRefresh(false); + } + }, this.siteId); } /** @@ -241,7 +267,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { }, this.siteId); } else { // Invalidate and refresh data to update this view. - this.invalidateAndRefresh(); + this.invalidateAndRefresh(true); } }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'core.error', true); @@ -281,17 +307,23 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { /** * Check if there's data to save (grade). * + * @param isSubmit Whether the user is about to submit the grade. * @return Promise resolved with boolean: whether there's data to save. */ - protected hasDataToSave(): Promise { + protected async hasDataToSave(isSubmit?: boolean): Promise { if (!this.canSaveGrades || !this.loaded) { - return Promise.resolve(false); + return false; + } + + if (isSubmit && this.hasOfflineGrade) { + // Always allow sending if the grade is saved in offline. + return true; } // Check if numeric grade and toggles changed. if (this.originalGrades.grade != this.grade.grade || this.originalGrades.addAttempt != this.grade.addAttempt || this.originalGrades.applyToAll != this.grade.applyToAll) { - return Promise.resolve(true); + return true; } // Check if outcomes changed. @@ -301,20 +333,21 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { if (this.originalGrades.outcomes[outcome.id] == 'undefined' || this.originalGrades.outcomes[outcome.id] != outcome.selectedId) { - return Promise.resolve(true); + return true; } } } if (this.feedback && this.feedback.plugins) { - return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId) - .catch(() => { + try { + return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId); + } catch (error) { // Error ocurred, consider there are no changes. return false; - }); + } } - return Promise.resolve(false); + return false; } /** @@ -334,9 +367,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { /** * Invalidate and refresh data. * + * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - invalidateAndRefresh(): Promise { + invalidateAndRefresh(sync?: boolean): Promise { this.loaded = false; const promises = []; @@ -361,16 +395,17 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { return Promise.all(promises).catch(() => { // Ignore errors. }).then(() => { - return this.loadData(); + return this.loadData(sync); }); } /** * Load the data to render the submission. * + * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - protected loadData(): Promise { + protected async loadData(sync?: boolean): Promise { let isBlind = !!this.blindId; this.previousAttempt = undefined; @@ -381,44 +416,53 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { isBlind = false; } - // Get the assignment. - return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { - const time = this.timeUtils.timestamp(), - promises = []; + try { + // Get the assignment. + this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId); - this.assign = assign; + if (this.submitId != this.currentUserId && sync) { + // Teacher viewing a student submission. Try to sync the assign, there could be offline grades stored. + try { + const result = await AddonModAssignSync.instance.syncAssign(this.assign.id); - if (assign.allowsubmissionsfromdate && assign.allowsubmissionsfromdate >= time) { - this.fromDate = this.timeUtils.userDate(assign.allowsubmissionsfromdate * 1000); + if (result && result.updated) { + this.eventsProvider.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, { + assignId: this.assign.id, + warnings: result.warnings, + gradesBlocked: result.gradesBlocked, + context: 'submission', + submitId: this.submitId, + }, this.siteId); + } + } catch (error) { + // Ignore errors, probably user is offline or sync is blocked. + } + } + + const time = this.timeUtils.timestamp(); + let promises = []; + + if (this.assign.allowsubmissionsfromdate && this.assign.allowsubmissionsfromdate >= time) { + this.fromDate = this.timeUtils.userDate(this.assign.allowsubmissionsfromdate * 1000); } this.currentAttempt = 0; this.maxAttemptsText = this.translate.instant('addon.mod_assign.unlimitedattempts'); - this.blindMarking = this.isSubmittedForGrading && assign.blindmarking && !assign.revealidentities; + this.blindMarking = this.isSubmittedForGrading && this.assign.blindmarking && !this.assign.revealidentities; if (!this.blindMarking && this.submitId != this.currentUserId) { - promises.push(this.userProvider.getProfile(this.submitId, this.courseId).then((profile) => { - this.user = profile; - })); + promises.push(this.loadSubmissionUserProfile()); } // Check if there's any offline data for this submission. - promises.push(this.assignOfflineProvider.getSubmission(assign.id, this.submitId).then((data) => { - this.hasOffline = data && data.plugindata && Object.keys(data.plugindata).length > 0; - this.submittedOffline = data && data.submitted; - }).catch(() => { - // No offline data found. - this.hasOffline = false; - this.submittedOffline = false; - })); + promises.push(this.loadSubmissionOfflineData()); + + await Promise.all(promises); - return Promise.all(promises); - }).then(() => { // Get submission status. - return this.assignProvider.getSubmissionStatusWithRetry(this.assign, this.submitId, undefined, isBlind); - }).then((response) => { + const response = await this.assignProvider.getSubmissionStatusWithRetry(this.assign, {userId: this.submitId, isBlind}); - const promises = []; + promises = []; this.submissionStatusAvailable = true; this.lastAttempt = response.lastattempt; @@ -450,16 +494,41 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { } // Get the submission plugins that don't support editing. - promises.push(this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins).then((list) => { - this.unsupportedEditPlugins = list; - })); + promises.push(this.loadUnsupportedPlugins()); - return Promise.all(promises); - }).catch((error) => { + await Promise.all(promises); + } catch (error) { this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); - }).finally(() => { + } finally { this.loaded = true; - }); + } + } + + /** + * Load profile of submission's user. + * + * @return Promise resolved when done. + */ + protected async loadSubmissionUserProfile(): Promise { + this.user = await this.userProvider.getProfile(this.submitId, this.courseId); + } + + /** + * Load offline data for the submission (not the submission grade). + * + * @return Promise resolved when done. + */ + protected async loadSubmissionOfflineData(): Promise { + try { + const data = await this.assignOfflineProvider.getSubmission(this.assign.id, this.submitId); + + this.hasOffline = data && data.plugindata && Object.keys(data.plugindata).length > 0; + this.submittedOffline = data && data.submitted; + } catch (error) { + // No offline data found. + this.hasOffline = false; + this.submittedOffline = false; + } } /** @@ -537,11 +606,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { // Make sure outcomes is an array. gradeInfo.outcomes = gradeInfo.outcomes || []; - if (!this.isDestroyed) { - // Block the assignment. - this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); - } - // Treat the grade info. return this.treatGradeInfo(); }).then(() => { @@ -589,11 +653,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { return this.assignOfflineProvider.getSubmissionGrade(this.assign.id, this.submitId).catch(() => { // Grade not found. }).then((data) => { + this.hasOfflineGrade = false; // Load offline grades. if (data && (!feedback || !feedback.gradeddate || feedback.gradeddate < data.timemodified)) { // If grade has been modified from gradebook, do not use offline. if (this.grade.modified < data.timemodified) { + this.hasOfflineGrade = true; this.grade.grade = !this.grade.scale ? this.utils.formatFloat(data.grade) : data.grade; this.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced'; this.gradingColor = ''; @@ -627,6 +693,15 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { }); } + /** + * Get the submission plugins that don't support editing. + * + * @return Promise resolved when done. + */ + protected async loadUnsupportedPlugins(): Promise { + this.unsupportedEditPlugins = await this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins); + } + /** * Set the submission status name and class. * @@ -725,7 +800,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { */ submitGrade(): Promise { // Check if there's something to be saved. - return this.hasDataToSave().then((modified) => { + return this.hasDataToSave(true).then((modified) => { if (!modified) { return; } @@ -764,7 +839,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { return this.discardDrafts(); }).finally(() => { // Invalidate and refresh data. - this.invalidateAndRefresh(); + this.invalidateAndRefresh(true); this.eventsProvider.trigger(AddonModAssignProvider.GRADED_EVENT, { assignmentId: this.assign.id, @@ -921,7 +996,9 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { response.lastattempt.submissiongroupmemberswhoneedtosubmit.forEach((member) => { if (this.blindMarking) { // Users not blinded! (Moodle < 3.1.1, 3.2). - promises.push(this.assignProvider.getAssignmentUserMappings(this.assign.id, member).then((blindId) => { + promises.push(this.assignProvider.getAssignmentUserMappings(this.assign.id, member, { + cmId: this.moduleId, + }).then((blindId) => { this.membersToSubmit.push(blindId); })); } else { @@ -952,15 +1029,42 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { } } + /** + * Block or unblock the automatic sync of the user grade. + * + * @param block Whether to block or unblock. + */ + protected setGradeSyncBlocked(block?: boolean): void { + if (this.isDestroyed || !this.assign || !this.isGrading) { + return; + } + + const syncId = AddonModAssignSync.instance.getGradeSyncId(this.assign.id, this.submitId); + + if (block) { + this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, syncId); + } else { + this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, syncId); + } + } + + /** + * A certain tab has been selected, either manually or automatically. + * + * @param tab The tab that was selected. + */ + tabSelected(tab: CoreTabComponent): void { + // Block sync when selecting grade tab, unblock when leaving it. + this.setGradeSyncBlocked(this.tabs.getIndex(tab) === 1); + } + /** * Component being destroyed. */ ngOnDestroy(): void { + this.setGradeSyncBlocked(false); this.isDestroyed = true; - - if (this.assign && this.isGrading) { - this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); - } + this.syncObserver && this.syncObserver.off(); } } diff --git a/src/addon/mod/assign/lang/en.json b/src/addon/mod/assign/lang/en.json index fd076f605..5311cdf8a 100644 --- a/src/addon/mod/assign/lang/en.json +++ b/src/addon/mod/assign/lang/en.json @@ -89,6 +89,7 @@ "submitassignment": "Submit assignment", "submittedearly": "Assignment was submitted {{$a}} early", "submittedlate": "Assignment was submitted {{$a}} late", + "syncblockedusercomponent": "user grade", "timemodified": "Last modified", "timeremaining": "Time remaining", "ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", diff --git a/src/addon/mod/assign/pages/edit/edit.ts b/src/addon/mod/assign/pages/edit/edit.ts index 33fcf85b8..d080ffd23 100644 --- a/src/addon/mod/assign/pages/edit/edit.ts +++ b/src/addon/mod/assign/pages/edit/edit.ts @@ -16,7 +16,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; @@ -125,11 +125,20 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { }).then(() => { // Get submission status. Ignore cache to get the latest data. - return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind, false, true) - .catch((err) => { + const options = { + userId: this.userId, + isBlind: this.isBlind, + cmId: this.assign.cmid, + filter: false, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }; + + return this.assignProvider.getSubmissionStatus(this.assign.id, options).catch((err) => { // Cannot connect. Get cached data. - return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind) - .then((response) => { + options.filter = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + return this.assignProvider.getSubmissionStatus(this.assign.id, options).then((response) => { const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt); // Check if the user can edit it in offline. diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.ts b/src/addon/mod/assign/pages/submission-list/submission-list.ts index 1c1429011..b0d6d2eca 100644 --- a/src/addon/mod/assign/pages/submission-list/submission-list.ts +++ b/src/addon/mod/assign/pages/submission-list/submission-list.ts @@ -15,7 +15,7 @@ import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreEventsProvider } from '@providers/events'; +import { CoreEventsProvider, CoreEventObserver } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; @@ -23,6 +23,7 @@ import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignGrade, AddonModAssignSubmission } from '../../providers/assign'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; +import { AddonModAssignSyncProvider, AddonModAssignSync } from '../../providers/assign-sync'; import { AddonModAssignHelperProvider, AddonModAssignSubmissionFormatted } from '../../providers/helper'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -54,10 +55,11 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { protected moduleId: number; // Module ID the submission belongs to. protected courseId: number; // Course ID the assignment belongs to. protected selectedStatus: string; // The status to see. - protected gradedObserver; // Observer to refresh data when a grade changes. + protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes. + protected syncObserver: CoreEventObserver; // OObserver to refresh data when the async is synchronized. protected submissionsData: {canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}; - constructor(navParams: NavParams, protected sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + constructor(navParams: NavParams, protected sitesProvider: CoreSitesProvider, protected eventsProvider: CoreEventsProvider, protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService, protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, protected assignHelper: AddonModAssignHelperProvider, protected groupsProvider: CoreGroupsProvider) { @@ -79,22 +81,37 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { // Update data if some grade changes. this.gradedObserver = eventsProvider.on(AddonModAssignProvider.GRADED_EVENT, (data) => { - if (this.assign && data.assignmentId == this.assign.id && data.userId == sitesProvider.getCurrentSiteUserId()) { + if (this.loaded && this.assign && data.assignmentId == this.assign.id && + data.userId == sitesProvider.getCurrentSiteUserId()) { // Grade changed, refresh the data. this.loaded = false; - this.refreshAllData().finally(() => { + this.refreshAllData(true).finally(() => { this.loaded = true; }); } }, sitesProvider.getCurrentSiteId()); + + // Refresh data if this assign is synchronized. + const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED]; + this.syncObserver = eventsProvider.onMultiple(events, (data) => { + if (!this.loaded || data.context == 'submission-list') { + return; + } + + this.loaded = false; + + this.refreshAllData(false).finally(() => { + this.loaded = true; + }); + }, this.sitesProvider.getCurrentSiteId()); } /** * Component being initialized. */ ngOnInit(): void { - this.fetchAssignment().finally(() => { + this.fetchAssignment(true).finally(() => { if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) { // Take first and load it. this.loadSubmission(this.submissions[0]); @@ -107,34 +124,49 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { /** * Fetch assignment data. * + * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - protected fetchAssignment(): Promise { + protected async fetchAssignment(sync?: boolean): Promise { + try { + // Get assignment data. + this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId); - // Get assignment data. - return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { - this.title = assign.name || this.title; - this.assign = assign; + this.title = this.assign.name || this.title; - // Get assignment submissions. - return this.assignProvider.getSubmissions(assign.id); - }).then((data) => { - if (!data.canviewsubmissions) { - // User shouldn't be able to reach here. - return Promise.reject(null); + if (sync) { + try { + // Try to synchronize data. + const result = await AddonModAssignSync.instance.syncAssign(this.assign.id); + + if (result && result.updated) { + this.eventsProvider.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, { + assignId: this.assign.id, + warnings: result.warnings, + gradesBlocked: result.gradesBlocked, + context: 'submission-list', + }, this.sitesProvider.getCurrentSiteId()); + } + } catch (error) { + // Ignore errors, probably user is offline or sync is blocked. + } } - this.submissionsData = data; + // Get assignment submissions. + this.submissionsData = await this.assignProvider.getSubmissions(this.assign.id, {cmId: this.assign.cmid}); + + if (!this.submissionsData.canviewsubmissions) { + // User shouldn't be able to reach here. + throw new Error('Cannot view submissions.'); + } // Check if groupmode is enabled to avoid showing wrong numbers. - return this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false).then((groupInfo) => { - this.groupInfo = groupInfo; + this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false); - return this.setGroup(this.groupsProvider.validateGroupId(this.groupId, groupInfo)); - }); - }).catch((error) => { + await this.setGroup(this.groupsProvider.validateGroupId(this.groupId, this.groupInfo)); + } catch (error) { this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); - }); + } } /** @@ -160,7 +192,8 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { const promises = [ this.assignHelper.getSubmissionsUserData(this.assign, this.submissionsData.submissions, this.groupId), // Get assignment grades only if workflow is not enabled to check grading date. - !this.assign.markingworkflow ? this.assignProvider.getAssignmentGrades(this.assign.id) : Promise.resolve(null), + !this.assign.markingworkflow ? this.assignProvider.getAssignmentGrades(this.assign.id, {cmId: this.assign.cmid}) : + Promise.resolve(null), ]; return Promise.all(promises).then(([submissions, grades]: [AddonModAssignSubmissionFormatted[], AddonModAssignGrade[]]) => { @@ -265,9 +298,10 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { /** * Refresh all the data. * + * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - protected refreshAllData(): Promise { + protected refreshAllData(sync?: boolean): Promise { const promises = []; promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); @@ -279,7 +313,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { } return Promise.all(promises).finally(() => { - return this.fetchAssignment(); + return this.fetchAssignment(sync); }); } @@ -289,7 +323,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { * @param refresher Refresher. */ refreshList(refresher: any): void { - this.refreshAllData().finally(() => { + this.refreshAllData(true).finally(() => { refresher.complete(); }); } @@ -299,6 +333,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.gradedObserver && this.gradedObserver.off(); + this.syncObserver && this.syncObserver.off(); } } diff --git a/src/addon/mod/assign/pages/submission-review/submission-review.ts b/src/addon/mod/assign/pages/submission-review/submission-review.ts index 030b7c112..d3d2ed5ba 100644 --- a/src/addon/mod/assign/pages/submission-review/submission-review.ts +++ b/src/addon/mod/assign/pages/submission-review/submission-review.ts @@ -137,7 +137,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit { } return Promise.all(promises).finally(() => { - this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); + this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); return this.fetchSubmission(); }); diff --git a/src/addon/mod/assign/providers/assign-sync.ts b/src/addon/mod/assign/providers/assign-sync.ts index 8b5bcfbb1..3cc10f7f6 100644 --- a/src/addon/mod/assign/providers/assign-sync.ts +++ b/src/addon/mod/assign/providers/assign-sync.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -25,12 +25,14 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; -import { AddonModAssignProvider, AddonModAssignAssign } from './assign'; +import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync'; +import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission } from './assign'; import { AddonModAssignOfflineProvider } from './assign-offline'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; +import { makeSingleton } from '@singletons/core.singletons'; + /** * Data returned by an assign sync. */ @@ -44,6 +46,11 @@ export interface AddonModAssignSyncResult { * Whether data was updated in the site. */ updated: boolean; + + /** + * Whether some grade couldn't be synced because it was blocked. + */ + gradesBlocked: number[]; } /** @@ -53,6 +60,7 @@ export interface AddonModAssignSyncResult { export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { static AUTO_SYNCED = 'addon_mod_assign_autom_synced'; + static MANUAL_SYNCED = 'addon_mod_assign_manual_synced'; protected componentTranslate: string; @@ -79,6 +87,17 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { this.componentTranslate = courseProvider.translateModuleName('assign'); } + /** + * Get the sync ID for a certain user grade. + * + * @param assignId Assign ID. + * @param userId User the grade belongs to. + * @return Sync ID. + */ + getGradeSyncId(assignId: number, userId: number): string { + return 'assignGrade#' + assignId + '#' + userId; + } + /** * Convenience function to get scale selected option. * @@ -121,7 +140,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { * @param force Wether to force sync not depending on last execution. * @return Promise resolved if sync is successful, rejected if sync fails. */ - syncAllAssignments(siteId?: string, force?: boolean): Promise { + syncAllAssignments(siteId?: string, force?: boolean): Promise { return this.syncOnSites('all assignments', this.syncAllAssignmentsFunc.bind(this), [force], siteId); } @@ -132,26 +151,25 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { * @param force Wether to force sync not depending on last execution. * @param Promise resolved if sync is successful, rejected if sync fails. */ - protected syncAllAssignmentsFunc(siteId?: string, force?: boolean): Promise { + protected async syncAllAssignmentsFunc(siteId?: string, force?: boolean): Promise { // Get all assignments that have offline data. - return this.assignOfflineProvider.getAllAssigns(siteId).then((assignIds) => { - // Sync all assignments that haven't been synced for a while. - const promises = assignIds.map((assignId) => { - const promise = force ? this.syncAssign(assignId, siteId) : this.syncAssignIfNeeded(assignId, siteId); + const assignIds = await this.assignOfflineProvider.getAllAssigns(siteId); - return promise.then((data) => { - if (data && data.updated) { - // Sync done. Send event. - this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, { - assignId: assignId, - warnings: data.warnings - }, siteId); - } - }); - }); + // Try to sync all assignments. + await Promise.all(assignIds.map(async (assignId) => { + const data = force ? await this.syncAssign(assignId, siteId) : await this.syncAssignIfNeeded(assignId, siteId); - return Promise.all(promises); - }); + if (!data || !data.updated) { + // Not updated. + return; + } + + this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, { + assignId: assignId, + warnings: data.warnings, + gradesBlocked: data.gradesBlocked, + }, siteId); + })); } /** @@ -161,12 +179,12 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the assign is synced or it doesn't need to be synced. */ - syncAssignIfNeeded(assignId: number, siteId?: string): Promise { - return this.isSyncNeeded(assignId, siteId).then((needed) => { - if (needed) { - return this.syncAssign(assignId, siteId); - } - }); + async syncAssignIfNeeded(assignId: number, siteId?: string): Promise { + const needed = await this.isSyncNeeded(assignId, siteId); + + if (needed) { + return this.syncAssign(assignId, siteId); + } } /** @@ -176,18 +194,9 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved in success. */ - syncAssign(assignId: number, siteId?: string): Promise { + async syncAssign(assignId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - const promises: Promise[] = [], - result: AddonModAssignSyncResult = { - warnings: [], - updated: false - }; - let assign: AddonModAssignAssign, - courseId: number, - syncPromise: Promise; - if (this.isSyncing(assignId, siteId)) { // There's already a sync ongoing for this assign, return the promise. return this.getOngoingSync(assignId, siteId); @@ -195,79 +204,126 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { // Verify that assign isn't blocked. if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) { - this.logger.debug('Cannot sync assign ' + assignId + ' because it is blocked.'); + this.logger.error('Cannot sync assign ' + assignId + ' because it is blocked.'); - return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); } + return this.addOngoingSync(assignId, this.performSyncAssign(assignId, siteId), siteId); + } + + /** + * Perform the assign submission. + * + * @param assignId Assign ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved in success. + */ + protected async performSyncAssign(assignId: number, siteId?: string): Promise { + this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId); - // Get offline submissions to be sent. - promises.push(this.assignOfflineProvider.getAssignSubmissions(assignId, siteId).catch(() => { - // No offline data found, return empty array. - return []; - })); + const result: AddonModAssignSyncResult = { + warnings: [], + updated: false, + gradesBlocked: [], + }; - // Get offline submission grades to be sent. - promises.push(this.assignOfflineProvider.getAssignSubmissionsGrade(assignId, siteId).catch(() => { - // No offline data found, return empty array. - return []; - })); + // Load offline data and sync offline logs. + const promisesResults = await Promise.all([ + this.getOfflineSubmissions(assignId, siteId), + this.getOfflineGrades(assignId, siteId), + this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId), + ]); - // Sync offline logs. - promises.push(this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId)); + const submissions = promisesResults[0]; + const grades = promisesResults[1]; - syncPromise = Promise.all(promises).then((results) => { - const submissions = results[0], - grades = results[1]; + if (!submissions.length && !grades.length) { + // Nothing to sync. + await this.utils.ignoreErrors(this.setSyncTime(assignId, siteId)); - if (!submissions.length && !grades.length) { - // Nothing to sync. - return; - } else if (!this.appProvider.isOnline()) { - // Cannot sync in offline. - return Promise.reject(null); - } - - courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; - - return this.assignProvider.getAssignmentById(courseId, assignId, false, siteId).then((assignData) => { - assign = assignData; - - const promises = []; - - submissions.forEach((submission) => { - promises.push(this.syncSubmission(assign, submission, result.warnings, siteId).then(() => { - result.updated = true; - })); - }); - - grades.forEach((grade) => { - promises.push(this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId).then(() => { - result.updated = true; - })); - }); - - return Promise.all(promises); - }).then(() => { - if (result.updated) { - // Data has been sent to server. Now invalidate the WS calls. - return this.assignProvider.invalidateContent(assign.cmid, courseId, siteId).catch(() => { - // Ignore errors. - }); - } - }); - }).then(() => { - // Sync finished, set sync time. - return this.setSyncTime(assignId, siteId).catch(() => { - // Ignore errors. - }); - }).then(() => { - // All done, return the result. return result; - }); + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + throw new Error(this.translate.instant('core.cannotconnect')); + } - return this.addOngoingSync(assignId, syncPromise, siteId); + const courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; + + const assign = await this.assignProvider.getAssignmentById(courseId, assignId, {siteId}); + + let promises = []; + + promises = promises.concat(submissions.map(async (submission) => { + await this.syncSubmission(assign, submission, result.warnings, siteId); + + result.updated = true; + })); + + promises = promises.concat(grades.map(async (grade) => { + try { + await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId); + + result.updated = true; + } catch (error) { + if (error instanceof CoreSyncBlockedError) { + // Grade blocked, but allow finish the sync. + result.gradesBlocked.push(grade.userid); + } else { + throw error; + } + } + })); + + await Promise.all(promises); + + if (result.updated) { + // Data has been sent to server. Now invalidate the WS calls. + await this.utils.ignoreErrors(this.assignProvider.invalidateContent(assign.cmid, courseId, siteId)); + } + + // Sync finished, set sync time. + await this.utils.ignoreErrors(this.setSyncTime(assignId, siteId)); + + // All done, return the result. + return result; + } + + /** + * Get offline grades to be sent. + * + * @param assignId Assign ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise with grades. + */ + protected async getOfflineGrades(assignId: number, siteId: string): Promise { + try { + const submissions = await this.assignOfflineProvider.getAssignSubmissionsGrade(assignId, siteId); + + return submissions; + } catch (error) { + // No offline data found, return empty array. + return []; + } + } + + /** + * Get offline submissions to be sent. + * + * @param assignId Assign ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise with submissions. + */ + protected async getOfflineSubmissions(assignId: number, siteId: string): Promise { + try { + const submissions = await this.assignOfflineProvider.getAssignSubmissions(assignId, siteId); + + return submissions; + } catch (error) { + // No offline data found, return empty array. + return []; + } } /** @@ -279,83 +335,82 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved if success, rejected otherwise. */ - protected syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string): Promise { - const userId = offlineData.userid, - pluginData = {}; - let discardError, - submission; + protected async syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string) + : Promise { - return this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId).then((status) => { - const promises = []; + const userId = offlineData.userid; + const pluginData = {}; + const options = { + userId, + cmId: assign.cmid, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; - submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt); + const status = await this.assignProvider.getSubmissionStatus(assign.id, options); - if (submission.timemodified != offlineData.onlinetimemodified) { - // The submission was modified in Moodle, discard the submission. - discardError = this.translate.instant('addon.mod_assign.warningsubmissionmodified'); + const submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt); - return; + if (submission.timemodified != offlineData.onlinetimemodified) { + // The submission was modified in Moodle, discard the submission. + this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, + this.translate.instant('addon.mod_assign.warningsubmissionmodified')); + + return this.deleteSubmissionData(assign, submission, offlineData, siteId); + } + + try { + // Prepare plugins data. + await Promise.all(submission.plugins.map(async (plugin) => { + await this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData, siteId); + })); + + // Now save the submission. + if (Object.keys(pluginData).length > 0) { + await this.assignProvider.saveSubmissionOnline(assign.id, pluginData, siteId); } - submission.plugins.forEach((plugin) => { - promises.push(this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData, - siteId)); - }); - - return Promise.all(promises).then(() => { - // Now save the submission. - let promise; - - if (!Object.keys(pluginData).length) { - // Nothing to save. - promise = Promise.resolve(); - } else { - promise = this.assignProvider.saveSubmissionOnline(assign.id, pluginData, siteId); - } - - return promise.then(() => { - if (assign.submissiondrafts && offlineData.submitted) { - // The user submitted the assign manually. Submit it for grading. - return this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionstatement, siteId); - } - }).then(() => { - // Submission data sent, update cached data. No need to block the user for this. - this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); - }); - }).catch((error) => { - if (error && this.utils.isWebServiceError(error)) { - // A WebService has thrown an error, this means it cannot be submitted. Discard the submission. - discardError = this.textUtils.getErrorMessageFromError(error); - } else { - // Couldn't connect to server, reject. - return Promise.reject(error); - } - }); - }).then(() => { - // Delete the offline data. - return this.assignOfflineProvider.deleteSubmission(assign.id, userId, siteId).then(() => { - const promises = []; - - submission.plugins.forEach((plugin) => { - promises.push(this.submissionDelegate.deletePluginOfflineData(assign, submission, plugin, offlineData, siteId)); - }); - - return Promise.all(promises); - }); - }).then(() => { - if (discardError) { - // Submission was discarded, add a warning. - const message = this.translate.instant('core.warningofflinedatadeleted', { - component: this.componentTranslate, - name: assign.name, - error: discardError - }); - - if (warnings.indexOf(message) == -1) { - warnings.push(message); - } + if (assign.submissiondrafts && offlineData.submitted) { + // The user submitted the assign manually. Submit it for grading. + await this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionstatement, siteId); } - }); + + // Submission data sent, update cached data. No need to block the user for this. + this.assignProvider.getSubmissionStatus(assign.id, options); + } catch (error) { + if (!error || !this.utils.isWebServiceError(error)) { + // Local error, reject. + throw error; + } + + // A WebService has thrown an error, this means it cannot be submitted. Discard the submission. + this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, + this.textUtils.getErrorMessageFromError(error)); + } + + // Delete the offline data. + await this.deleteSubmissionData(assign, submission, offlineData, siteId); + } + + /** + * Delete the submission offline data (not grades). + * + * @param assign Assign. + * @param submission Submission. + * @param offlineData Offline data. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async deleteSubmissionData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, offlineData: any, + siteId?: string): Promise { + + // Delete the offline data. + await this.assignOfflineProvider.deleteSubmission(assign.id, offlineData.userid, siteId); + + // Delete plugins data. + await Promise.all(submission.plugins.map(async (plugin) => { + await this.submissionDelegate.deletePluginOfflineData(assign, submission, plugin, offlineData, siteId); + })); } /** @@ -368,90 +423,95 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved if success, rejected otherwise. */ - protected syncSubmissionGrade(assign: AddonModAssignAssign, offlineData: any, warnings: string[], courseId: number, + protected async syncSubmissionGrade(assign: AddonModAssignAssign, offlineData: any, warnings: string[], courseId: number, siteId?: string): Promise { const userId = offlineData.userid; - let discardError; + const syncId = this.getGradeSyncId(assign.id, userId); + const options = { + userId, + cmId: assign.cmid, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; - return this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId).then((status) => { - const timemodified = status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified); + // Check if this grade sync is blocked. + if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, syncId, siteId)) { + this.logger.error(`Cannot sync grade for assign ${assign.id} and user ${userId} because it is blocked.!!!!`); - if (timemodified > offlineData.timemodified) { - // The submission grade was modified in Moodle, discard it. - discardError = this.translate.instant('addon.mod_assign.warningsubmissiongrademodified'); + throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked', + {$a: this.translate.instant('addon.mod_assign.syncblockedusercomponent')})); + } - return; - } + const status = await this.assignProvider.getSubmissionStatus(assign.id, options); - // If grade has been modified from gradebook, do not use offline. - return this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true).then((grades) => { - return this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId).then((gradeInfo) => { + const timemodified = status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified); - // Override offline grade and outcomes based on the gradebook data. - grades.forEach((grade) => { - if (grade.gradedategraded >= offlineData.timemodified) { - if (!grade.outcomeid && !grade.scaleid) { - if (gradeInfo && gradeInfo.scale) { - offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.gradeformatted); - } else { - offlineData.grade = parseFloat(grade.gradeformatted) || null; - } - } else if (grade.outcomeid && this.assignProvider.isOutcomesEditEnabled() && gradeInfo.outcomes) { - gradeInfo.outcomes.forEach((outcome, index) => { - if (outcome.scale && grade.itemnumber == index) { - offlineData.outcomes[grade.itemnumber] = this.getSelectedScaleId(outcome.scale, - outcome.selected); - } - }); - } + if (timemodified > offlineData.timemodified) { + // The submission grade was modified in Moodle, discard it. + this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, + this.translate.instant('addon.mod_assign.warningsubmissiongrademodified')); + + return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId); + } + + // If grade has been modified from gradebook, do not use offline. + const grades = await this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true); + + const gradeInfo = await this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId); + + // Override offline grade and outcomes based on the gradebook data. + grades.forEach((grade) => { + if (grade.gradedategraded >= offlineData.timemodified) { + if (!grade.outcomeid && !grade.scaleid) { + if (gradeInfo && gradeInfo.scale) { + offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.gradeformatted); + } else { + offlineData.grade = parseFloat(grade.gradeformatted) || null; + } + } else if (grade.outcomeid && this.assignProvider.isOutcomesEditEnabled() && gradeInfo.outcomes) { + gradeInfo.outcomes.forEach((outcome, index) => { + if (outcome.scale && grade.itemnumber == index) { + offlineData.outcomes[grade.itemnumber] = this.getSelectedScaleId(outcome.scale, + outcome.selected); } }); - }); - }).then(() => { - // Now submit the grade. - return this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptnumber, - offlineData.addattempt, offlineData.workflowstate, offlineData.applytoall, offlineData.outcomes, - offlineData.plugindata, siteId).then(() => { - // Grades sent. - // Discard grades drafts. - const promises = []; - if (status.feedback && status.feedback.plugins) { - status.feedback.plugins.forEach((plugin) => { - promises.push(this.feedbackDelegate.discardPluginFeedbackData(assign.id, userId, plugin, siteId)); - }); - } - - // Update cached data. - promises.push(this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId)); - - return Promise.all(promises); - }).catch((error) => { - if (error && this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means it cannot be submitted. Discard the offline data. - discardError = this.textUtils.getErrorMessageFromError(error); - } else { - // Couldn't connect to server, reject. - return Promise.reject(error); - } - }); - }); - }).then(() => { - // Delete the offline data. - return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId); - }).then(() => { - if (discardError) { - // Submission grade was discarded, add a warning. - const message = this.translate.instant('core.warningofflinedatadeleted', { - component: this.componentTranslate, - name: assign.name, - error: discardError - }); - - if (warnings.indexOf(message) == -1) { - warnings.push(message); } } }); + + try { + // Now submit the grade. + await this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptnumber, + offlineData.addattempt, offlineData.workflowstate, offlineData.applytoall, offlineData.outcomes, + offlineData.plugindata, siteId); + + // Grades sent. Discard grades drafts. + const promises = []; + if (status.feedback && status.feedback.plugins) { + status.feedback.plugins.forEach((plugin) => { + promises.push(this.feedbackDelegate.discardPluginFeedbackData(assign.id, userId, plugin, siteId)); + }); + } + + // Update cached data. + promises.push(this.assignProvider.getSubmissionStatus(assign.id, options)); + + await Promise.all(promises); + } catch (error) { + if (!error || !this.utils.isWebServiceError(error)) { + // Local error, reject. + throw error; + } + + // A WebService has thrown an error, this means it cannot be submitted. Discard the submission. + this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, + this.textUtils.getErrorMessageFromError(error)); + } + + // Delete the offline data. + await this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId); } } + +export class AddonModAssignSync extends makeSingleton(AddonModAssignSyncProvider) {} diff --git a/src/addon/mod/assign/providers/assign.ts b/src/addon/mod/assign/providers/assign.ts index 2ab77ae3a..cf570beb7 100644 --- a/src/addon/mod/assign/providers/assign.ts +++ b/src/addon/mod/assign/providers/assign.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -25,9 +25,10 @@ import { CoreGradesProvider } from '@core/grades/providers/grades'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignOfflineProvider } from './assign-offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreInterceptor } from '@classes/interceptor'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some functions for assign. @@ -143,12 +144,11 @@ export class AddonModAssignProvider { * * @param courseId Course ID the assignment belongs to. * @param cmId Assignment module ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the assignment. */ - getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId); + getAssignment(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getAssignmentByField(courseId, 'cmid', cmId, options); } /** @@ -157,27 +157,23 @@ export class AddonModAssignProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the assignment is retrieved. */ - protected getAssignmentByField(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) + protected getAssignmentByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId], - includenotenrolledcourses: 1 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAssignmentCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + includenotenrolledcourses: 1, + }; + const preSets = { + cacheKey: this.getAssignmentCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModAssignProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_assignments', params, preSets).catch(() => { // In 3.6 we added a new parameter includenotenrolledcourses that could cause offline data not to be found. @@ -206,13 +202,12 @@ export class AddonModAssignProvider { * Get an assignment by instance ID. * * @param courseId Course ID the assignment belongs to. - * @param cmId Assignment instance ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param id Assignment instance ID. + * @param options Other options. * @return Promise resolved with the assignment. */ - getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId); + getAssignmentById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getAssignmentByField(courseId, 'id', id, options); } /** @@ -230,24 +225,22 @@ export class AddonModAssignProvider { * * @param assignId Assignment Id. * @param userId User Id to be blinded. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the user blind id. */ - getAssignmentUserMappings(assignId: number, userId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - assignmentids: [assignId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAssignmentUserMappingsCacheKey(assignId), - updateFrequency: CoreSite.FREQUENCY_OFTEN - }; + getAssignmentUserMappings(assignId: number, userId: number, options: CoreCourseCommonModWSOptions = {}): Promise { - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + assignmentids: [assignId], + }; + const preSets = { + cacheKey: this.getAssignmentUserMappingsCacheKey(assignId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_user_mappings', params, preSets) .then((response: AddonModAssignGetUserMappingsResult): any => { @@ -293,23 +286,21 @@ export class AddonModAssignProvider { * Returns grade information from assign_grades for the requested assignment id * * @param assignId Assignment Id. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with requested info when done. */ - getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - assignmentids: [assignId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAssignmentGradesCacheKey(assignId) - }; + getAssignmentGrades(assignId: number, options: CoreCourseCommonModWSOptions = {}): Promise { - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + assignmentids: [assignId], + }; + const preSets = { + cacheKey: this.getAssignmentGradesCacheKey(assignId), + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_grades', params, preSets).then((response: AddonModAssignGetGradesResult): any => { // Search the assignment. @@ -455,26 +446,23 @@ export class AddonModAssignProvider { * Get an assignment submissions. * * @param assignId Assignment id. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - getSubmissions(assignId: number, ignoreCache?: boolean, siteId?: string) + getSubmissions(assignId: number, options: CoreCourseCommonModWSOptions = {}) : Promise<{canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}> { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - assignmentids: [assignId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubmissionsCacheKey(assignId), - updateFrequency: CoreSite.FREQUENCY_OFTEN - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + assignmentids: [assignId], + }; + const preSets = { + cacheKey: this.getSubmissionsCacheKey(assignId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_submissions', params, preSets) .then((response: AddonModAssignGetSubmissionsResult): any => { @@ -510,46 +498,40 @@ export class AddonModAssignProvider { * Get information about an assignment submission status for a given user. * * @param assignId Assignment instance id. - * @param userId User Id (empty for current user). - * @param groupId Group Id (empty for all participants). - * @param isBlind If blind marking is enabled or not. - * @param filter True to filter WS response and rewrite URLs, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site id (empty for current site). + * @param options Other options. * @return Promise always resolved with the user submission status. */ - getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, - ignoreCache?: boolean, siteId?: string): Promise { + getSubmissionStatus(assignId: number, options: AddonModAssignSubmissionStatusOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const fixedParams = this.fixSubmissionStatusParams(site, userId, groupId, isBlind); + if (options.filter === undefined || options.filter === null) { + options.filter = true; + } + + return this.sitesProvider.getSite(options.siteId).then((site) => { + const fixedParams = this.fixSubmissionStatusParams(site, options.userId, options.groupId, options.isBlind); const params = { - assignid: assignId, - userid: fixedParams.userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId, - fixedParams.isBlind), - getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account. - filter: filter, - rewriteurls: filter - }; - + assignid: assignId, + userid: fixedParams.userId, + }; if (fixedParams.groupId) { params['groupid'] = fixedParams.groupId; } - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - - if (!filter) { + const preSets = { + cacheKey: this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId, + fixedParams.isBlind), + getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account. + filter: options.filter, + rewriteurls: options.filter, + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, // Don't cache when getting text without filters. // @todo Change this to support offline editing. - preSets.saveToCache = false; - } + saveToCache: options.filter, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_submission_status', params, preSets); }); @@ -560,23 +542,24 @@ export class AddonModAssignProvider { * If the data doesn't include the user submission, retry ignoring cache. * * @param assign Assignment. - * @param userId User id (empty for current user). - * @param groupId Group Id (empty for all participants). - * @param isBlind If blind marking is enabled or not. - * @param filter True to filter WS response and rewrite URLs, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site id (empty for current site). + * @param options Other options. * @return Promise always resolved with the user submission status. */ - getSubmissionStatusWithRetry(assign: any, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, - ignoreCache?: boolean, siteId?: string): Promise { + getSubmissionStatusWithRetry(assign: any, options: AddonModAssignSubmissionStatusOptions = {}) + : Promise { + options.cmId = options.cmId || assign.cmid; - return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, ignoreCache, siteId).then((response) => { + return this.getSubmissionStatus(assign.id, options).then((response) => { const userSubmission = this.getSubmissionObjectFromAttempt(assign, response.lastattempt); if (!userSubmission) { // Try again, ignoring cache. - return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, true, siteId).catch(() => { + const newOptions = { + ...options, // Include all the original options. + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }; + + return this.getSubmissionStatus(assign.id, newOptions).catch(() => { // Error, return the first result even if it doesn't have the user submission. return response; }); @@ -650,35 +633,32 @@ export class AddonModAssignProvider { * * @param assignId Assignment id. * @param groupId Group id. If not defined, 0. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of participants and summary of submissions. */ - listParticipants(assignId: number, groupId?: number, ignoreCache?: boolean, siteId?: string) + listParticipants(assignId: number, groupId?: number, options: CoreCourseCommonModWSOptions = {}) : Promise { groupId = groupId || 0; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { if (!site.wsAvailable('mod_assign_list_participants')) { // Silently fail if is not available. (needs Moodle version >= 3.2) return Promise.reject(null); } const params = { - assignid: assignId, - groupid: groupId, - filter: '' - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.listParticipantsCacheKey(assignId, groupId), - updateFrequency: CoreSite.FREQUENCY_OFTEN - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + assignid: assignId, + groupid: groupId, + filter: '', + }; + const preSets = { + cacheKey: this.listParticipantsCacheKey(assignId, groupId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_list_participants', params, preSets); }); @@ -769,7 +749,7 @@ export class AddonModAssignProvider { invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.getAssignment(courseId, moduleId, false, siteId).then((assign) => { + return this.getAssignment(courseId, moduleId, {siteId}).then((assign) => { const promises = []; // Do not invalidate assignment data before getting assignment info, we need it! @@ -1014,7 +994,10 @@ export class AddonModAssignProvider { } // We need more data to decide that. - return this.getSubmissionStatus(assignId, submission.submitid, undefined, submission.blindid).then((response) => { + return this.getSubmissionStatus(assignId, { + userId: submission.submitid, + isBlind: !!submission.blindid, + }).then((response) => { if (!response.feedback || !response.feedback.gradeddate) { // Not graded. return true; @@ -1304,6 +1287,16 @@ export class AddonModAssignProvider { } } +/** + * Options to pass to get submission status. + */ +export type AddonModAssignSubmissionStatusOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User Id (empty for current user). + groupId?: number; // Group Id (empty for all participants). + isBlind?: boolean; // If blind marking is enabled or not. + filter?: boolean; // True to filter WS response and rewrite URLs, false otherwise. Defaults to true. +}; + /** * Assign data returned by mod_assign_get_assignments. */ diff --git a/src/addon/mod/assign/providers/helper.ts b/src/addon/mod/assign/providers/helper.ts index 7cea16eef..4e7cc288e 100644 --- a/src/addon/mod/assign/providers/helper.ts +++ b/src/addon/mod/assign/providers/helper.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileProvider } from '@providers/file'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; @@ -209,29 +209,29 @@ export class AddonModAssignHelperProvider { * * @param assign Assignment object. * @param groupId Group Id. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of participants and summary of submissions. */ - getParticipants(assign: AddonModAssignAssign, groupId?: number, ignoreCache?: boolean, siteId?: string) + getParticipants(assign: AddonModAssignAssign, groupId?: number, options: CoreSitesCommonWSOptions = {}) : Promise { groupId = groupId || 0; - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.assignProvider.listParticipants(assign.id, groupId, ignoreCache, siteId).then((participants) => { + const modOptions = {cmId: assign.cmid, ...options}; // Create new options including all existing ones. + + return this.assignProvider.listParticipants(assign.id, groupId, modOptions).then((participants) => { if (groupId || participants && participants.length > 0) { return participants; } // If no participants returned and all groups specified, get participants by groups. - return this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((info) => { + return this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, modOptions.siteId).then((info) => { const promises = [], participants: {[id: number]: AddonModAssignParticipant} = {}; info.groups.forEach((userGroup) => { - promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, ignoreCache, siteId) - .then((parts) => { + promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, modOptions).then((parts) => { // Do not get repeated users. parts.forEach((participant) => { participants[participant.id] = participant; @@ -355,14 +355,15 @@ export class AddonModAssignHelperProvider { * @param assign Assignment object. * @param submissions Submissions to get the data for. * @param groupId Group Id. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site id (empty for current site). + * @param options Other options. * @return Promise always resolved. Resolve param is the formatted submissions. */ getSubmissionsUserData(assign: AddonModAssignAssign, submissions: AddonModAssignSubmissionFormatted[], groupId?: number, - ignoreCache?: boolean, siteId?: string): Promise { + options: CoreSitesCommonWSOptions = {}): Promise { - return this.getParticipants(assign, groupId).then((parts) => { + const modOptions = {cmId: assign.cmid, ...options}; // Create new options including all existing ones. + + return this.getParticipants(assign, groupId, modOptions).then((parts) => { const blind = assign.blindmarking && !assign.revealidentities; const promises = []; const result: AddonModAssignSubmissionFormatted[] = []; @@ -399,8 +400,8 @@ export class AddonModAssignHelperProvider { // Blind but not blinded! (Moodle < 3.1.1, 3.2). delete submission.userid; - promise = this.assignProvider.getAssignmentUserMappings(assign.id, submission.submitid, ignoreCache, siteId). - then((blindId) => { + promise = this.assignProvider.getAssignmentUserMappings(assign.id, submission.submitid, modOptions) + .then((blindId) => { submission.blindid = blindId; }); } diff --git a/src/addon/mod/assign/providers/prefetch-handler.ts b/src/addon/mod/assign/providers/prefetch-handler.ts index e67846720..26e2a11c4 100644 --- a/src/addon/mod/assign/providers/prefetch-handler.ts +++ b/src/addon/mod/assign/providers/prefetch-handler.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreGroupsProvider } from '@providers/groups'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -80,13 +80,13 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan canUseCheckUpdates(module: any, courseId: number): boolean | Promise { // Teachers cannot use the WS because it doesn't check student submissions. return this.assignProvider.getAssignment(courseId, module.id).then((assign) => { - return this.assignProvider.getSubmissions(assign.id).then((data) => { + return this.assignProvider.getSubmissions(assign.id, {cmId: module.id}).then((data) => { if (data.canviewsubmissions) { return false; } // Check if the user can view their own submission. - return this.assignProvider.getSubmissionStatus(assign.id).then(() => { + return this.assignProvider.getSubmissionStatus(assign.id, {cmId: module.id}).then(() => { return true; }); }); @@ -108,18 +108,18 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.assignProvider.getAssignment(courseId, module.id, false, siteId).then((assign) => { + return this.assignProvider.getAssignment(courseId, module.id, {siteId}).then((assign) => { // Get intro files and attachments. let files = assign.introattachments || []; files = files.concat(this.getIntroFilesFromInstance(module, assign)); // Now get the files in the submissions. - return this.assignProvider.getSubmissions(assign.id, false, siteId).then((data) => { + return this.assignProvider.getSubmissions(assign.id, {cmId: module.id, siteId}).then((data) => { const blindMarking = assign.blindmarking && !assign.revealidentities; if (data.canviewsubmissions) { // Teacher, get all submissions. - return this.assignHelper.getSubmissionsUserData(assign, data.submissions, 0, false, siteId) + return this.assignHelper.getSubmissionsUserData(assign, data.submissions, 0, {siteId}) .then((submissions: AddonModAssignSubmissionFormatted[]) => { const promises = []; @@ -172,8 +172,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string) : Promise { - return this.assignProvider.getSubmissionStatusWithRetry(assign, submitId, undefined, blindMarking, true, false, siteId) - .then((response) => { + return this.assignProvider.getSubmissionStatusWithRetry(assign, { + userId: submitId, + isBlind: blindMarking, + siteId, + }).then((response) => { const promises = []; let userSubmission: AddonModAssignSubmission; @@ -261,20 +264,24 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when done. */ protected prefetchAssign(module: any, courseId: number, single: boolean, siteId: string): Promise { - const userId = this.sitesProvider.getCurrentSiteUserId(), - promises = []; + const userId = this.sitesProvider.getCurrentSiteUserId(); + const promises = []; siteId = siteId || this.sitesProvider.getCurrentSiteId(); + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + // Get assignment to retrieve all its submissions. - promises.push(this.assignProvider.getAssignment(courseId, module.id, true, siteId).then((assign) => { + promises.push(this.assignProvider.getAssignment(courseId, module.id, options).then((assign) => { const subPromises = [], blindMarking = assign.blindmarking && !assign.revealidentities; if (blindMarking) { - subPromises.push(this.assignProvider.getAssignmentUserMappings(assign.id, undefined, true, siteId).catch(() => { - // Ignore errors. - })); + subPromises.push(this.utils.ignoreErrors(this.assignProvider.getAssignmentUserMappings(assign.id, -1, options))); } subPromises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId)); @@ -304,8 +311,14 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when prefetched, rejected otherwise. */ protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise { + const options = { + cmId: moduleId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + // Get submissions. - return this.assignProvider.getSubmissions(assign.id, true, siteId).then((data) => { + return this.assignProvider.getSubmissions(assign.id, options).then((data) => { const promises = []; promises.push(this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((groupInfo) => { @@ -317,14 +330,22 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan } groupInfo.groups.forEach((group) => { - groupProms.push(this.assignHelper.getSubmissionsUserData(assign, data.submissions, group.id, true, siteId) + groupProms.push(this.assignHelper.getSubmissionsUserData(assign, data.submissions, group.id, options) .then((submissions: AddonModAssignSubmissionFormatted[]) => { const subPromises = []; submissions.forEach((submission) => { - subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submission.submitid, - group.id, !!submission.blindid, true, true, siteId).then((subm) => { + const submissionOptions = { + userId: submission.submitid, + groupId: group.id, + isBlind: !!submission.blindid, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + + subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submissionOptions) + .then((subm) => { return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId); }).catch((error) => { if (error && error.errorcode == 'nopermission') { @@ -338,14 +359,21 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan if (!assign.markingworkflow) { // Get assignment grades only if workflow is not enabled to check grading date. - subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, true, siteId)); + subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, options)); } // Prefetch the submission of the current user even if it does not exist, this will be create it. if (!data.submissions || !data.submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) { - subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, userId, group.id, - false, true, true, siteId).then((subm) => { + const submissionOptions = { + userId, + groupId: group.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + + subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submissionOptions) + .then((subm) => { return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); })); } @@ -353,7 +381,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan return Promise.all(subPromises); }).then(() => { // Participiants already fetched, we don't need to ignore cache now. - return this.assignHelper.getParticipants(assign, group.id, false, siteId).then((participants) => { + return this.assignHelper.getParticipants(assign, group.id, {siteId}).then((participants) => { return this.userProvider.prefetchUserAvatars(participants, 'profileimageurl', siteId); }).catch(() => { // Fail silently (Moodle < 3.2). @@ -367,8 +395,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan // Prefetch own submission, we need to do this for teachers too so the response with error is cached. promises.push( - this.assignProvider.getSubmissionStatusWithRetry(assign, userId, undefined, false, true, true, siteId) - .then((subm) => { + this.assignProvider.getSubmissionStatusWithRetry(assign, { + userId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((subm) => { return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); }).catch((error) => { // Ignore if the user can't view their own submission. diff --git a/src/addon/mod/book/components/index/addon-mod-book-index.html b/src/addon/mod/book/components/index/addon-mod-book-index.html index ee1e5bbad..74ad51a52 100644 --- a/src/addon/mod/book/components/index/addon-mod-book-index.html +++ b/src/addon/mod/book/components/index/addon-mod-book-index.html @@ -9,7 +9,7 @@ - + diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts index 95d3264fd..a9e4f143b 100644 --- a/src/addon/mod/book/providers/book.ts +++ b/src/addon/mod/book/providers/book.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileProvider } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -73,11 +73,11 @@ export class AddonModBookProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the book is retrieved. */ - getBook(courseId: number, cmId: number, siteId?: string): Promise { - return this.getBookByField(courseId, 'coursemodule', cmId, siteId); + getBook(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getBookByField(courseId, 'coursemodule', cmId, options); } /** @@ -89,15 +89,19 @@ export class AddonModBookProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the book is retrieved. */ - protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getBookByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getBookDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId] + }; + const preSets = { + cacheKey: this.getBookDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModBookProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_book_get_books_by_courses', params, preSets) .then((response: AddonModBookGetBooksByCoursesResult): any => { diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts index 3130f397c..f5f50d7ba 100644 --- a/src/addon/mod/chat/pages/chat/chat.ts +++ b/src/addon/mod/chat/pages/chat/chat.ts @@ -127,7 +127,8 @@ export class AddonModChatChatPage { showChatUsers(): void { // Create the toc modal. const modal = this.modalCtrl.create('AddonModChatUsersPage', { - sessionId: this.sessionId + sessionId: this.sessionId, + cmId: this.cmId, }, { cssClass: 'core-modal-lateral', showBackdrop: true, enableBackdropDismiss: true, @@ -168,7 +169,7 @@ export class AddonModChatChatPage { return Promise.resolve(user.fullname); } - return this.chatProvider.getChatUsers(this.sessionId).then((data) => { + return this.chatProvider.getChatUsers(this.sessionId, {cmId: this.cmId}).then((data) => { this.users = data.users; const user = this.users.find((user) => user.id == id); diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.ts b/src/addon/mod/chat/pages/session-messages/session-messages.ts index 39e15433e..aa894c218 100644 --- a/src/addon/mod/chat/pages/session-messages/session-messages.ts +++ b/src/addon/mod/chat/pages/session-messages/session-messages.ts @@ -60,8 +60,8 @@ export class AddonModChatSessionMessagesPage { * @return Promise resolved when done. */ protected fetchMessages(): Promise { - return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId) - .then((messages) => { + return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId, + {cmId: this.cmId}).then((messages) => { return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => { this.messages = messages; diff --git a/src/addon/mod/chat/pages/sessions/sessions.ts b/src/addon/mod/chat/pages/sessions/sessions.ts index 997341b4c..2b62d4ad9 100644 --- a/src/addon/mod/chat/pages/sessions/sessions.ts +++ b/src/addon/mod/chat/pages/sessions/sessions.ts @@ -72,7 +72,7 @@ export class AddonModChatSessionsPage { this.groupInfo = groupInfo; this.groupId = this.groupsProvider.validateGroupId(this.groupId, groupInfo); - return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll); + return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll, {cmId: this.cmId}); }).then((sessions: AddonModChatSessionFormatted[]) => { // Fetch user profiles. const promises = []; diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts index e3e3c4028..6879416e9 100644 --- a/src/addon/mod/chat/pages/users/users.ts +++ b/src/addon/mod/chat/pages/users/users.ts @@ -36,6 +36,7 @@ export class AddonModChatUsersPage { isOnline: boolean; protected sessionId: string; + protected cmId: number; protected onlineObserver: any; constructor(navParams: NavParams, network: Network, zone: NgZone, private appProvider: CoreAppProvider, @@ -56,7 +57,7 @@ export class AddonModChatUsersPage { * View loaded. */ ionViewDidLoad(): void { - this.chatProvider.getChatUsers(this.sessionId).then((data) => { + this.chatProvider.getChatUsers(this.sessionId, {cmId: this.cmId}).then((data) => { this.users = data.users; }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhilegettingchatusers', true); diff --git a/src/addon/mod/chat/providers/chat.ts b/src/addon/mod/chat/providers/chat.ts index 8fa311954..688c051fe 100644 --- a/src/addon/mod/chat/providers/chat.ts +++ b/src/addon/mod/chat/providers/chat.ts @@ -14,13 +14,14 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; import { AddonModChatMessageForView, AddonModChatSessionMessageForView } from './helper'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for chats. @@ -40,17 +41,19 @@ export class AddonModChatProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the chat is retrieved. */ - getChat(courseId: number, cmId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getChat(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { courseids: [courseId] }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getChatsCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChatProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_chat_get_chats_by_courses', params, preSets) @@ -179,17 +182,25 @@ export class AddonModChatProvider { * Get the actives users of a current chat. * * @param sessionId Chat sessiond ID. + * @param options Other options. * @return Promise resolved when the WS is executed. */ - getChatUsers(sessionId: string): Promise { - const params = { - chatsid: sessionId - }; - const preSets = { - getFromCache: false - }; + getChatUsers(sessionId: string, options: CoreCourseCommonModWSOptions = {}): Promise { + // By default, always try to get the latest data. + options.readingStrategy = options.readingStrategy || CoreSitesReadingStrategy.PreferNetwork; - return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + chatsid: sessionId, + }; + const preSets = { + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_chat_get_chat_users', params, preSets); + }); } /** @@ -210,28 +221,26 @@ export class AddonModChatProvider { * @param chatId Chat ID. * @param groupId Group ID, 0 means that the function will determine the user group. * @param showAll Whether to include incomplete sessions or not. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of sessions. * @since 3.5 */ - getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, ignoreCache: boolean = false, siteId?: string): + getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { chatid: chatId, groupid: groupId, - showall: showAll ? 1 : 0 + showall: showAll ? 1 : 0, }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getSessionsCacheKey(chatId, groupId, showAll), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } return site.read('mod_chat_get_sessions', params, preSets).then((response: AddonModChatGetSessionsResult): any => { if (!response || !response.sessions) { @@ -250,29 +259,27 @@ export class AddonModChatProvider { * @param sessionStart Session start time. * @param sessionEnd Session end time. * @param groupId Group ID, 0 means that the function will determine the user group. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of messages. * @since 3.5 */ - getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, ignoreCache: boolean = false, - siteId?: string): Promise { + getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, + options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { chatid: chatId, sessionstart: sessionStart, sessionend: sessionEnd, - groupid: groupId + groupid: groupId, }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getSessionMessagesCacheKey(chatId, sessionStart, groupId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } return site.read('mod_chat_get_session_messages', params, preSets) .then((response: AddonModChatGetSessionMessagesResult): any => { diff --git a/src/addon/mod/chat/providers/prefetch-handler.ts b/src/addon/mod/chat/providers/prefetch-handler.ts index f5c5900b1..62821a577 100644 --- a/src/addon/mod/chat/providers/prefetch-handler.ts +++ b/src/addon/mod/chat/providers/prefetch-handler.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -122,9 +122,14 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise { // Prefetch chat and group info. const promises: Promise[] = [ - this.chatProvider.getChat(courseId, module.id, siteId), + this.chatProvider.getChat(courseId, module.id, {readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, siteId}), this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId) ]; + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; return Promise.all(promises).then(([chat, groupInfo]: [AddonModChatChat, CoreGroupInfo]) => { const promises = []; @@ -136,7 +141,7 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl groupIds.forEach((groupId) => { // Prefetch complete sessions. - promises.push(this.chatProvider.getSessions(chat.id, groupId, false, true, siteId).catch((error) => { + promises.push(this.chatProvider.getSessions(chat.id, groupId, false, options).catch((error) => { // Ignore group error. if (error.errorcode != 'notingroup') { return Promise.reject(error); @@ -144,8 +149,9 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl })); // Prefetch all sessions. - promises.push(this.chatProvider.getSessions(chat.id, groupId, true, true, siteId).then((sessions) => { - const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, siteId)); + promises.push(this.chatProvider.getSessions(chat.id, groupId, true, options).then((sessions) => { + const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, module.id, + siteId)); return Promise.all(promises); }).catch((error) => { @@ -170,9 +176,13 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param siteId Site ID. * @return Promise resolved when done. */ - protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise { - return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId) - .then((messages) => { + protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, cmId: number, siteId: string) + : Promise { + return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, { + cmId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((messages) => { const users = {}; session.sessionusers.forEach((user) => { users[user.userid] = true; diff --git a/src/addon/mod/choice/components/index/addon-mod-choice-index.html b/src/addon/mod/choice/components/index/addon-mod-choice-index.html index bc5de5829..bdadfd1f2 100644 --- a/src/addon/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addon/mod/choice/components/index/addon-mod-choice-index.html @@ -7,7 +7,7 @@ - + @@ -43,13 +43,17 @@ - {{ 'addon.mod_choice.full' | translate }} + + + - {{ 'addon.mod_choice.full' | translate }} + + + @@ -81,6 +85,7 @@

{{ 'addon.mod_choice.numberofuser' | translate }}: {{ result.numberofuser }} ({{ 'core.percentagenumber' | translate: {$a: result.percentageamountfixed} }})

+

{{ 'addon.mod_choice.limita' | translate:{$a: result.maxanswer} }}

@@ -95,3 +100,14 @@

{{ 'addon.mod_choice.noresultsviewable' | translate }}

+ + + +

+ {{ 'addon.mod_choice.full' | translate }} +

+ +

{{ 'addon.mod_choice.responsesa' | translate:{$a: option.countanswers} }}

+

{{ 'addon.mod_choice.limita' | translate:{$a: option.maxanswers} }}

+
+
diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index 2ce8f309b..89dbcc0dc 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -174,7 +174,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo * @return Promise resolved when done. */ protected fetchOptions(hasOffline: boolean): Promise { - return this.choiceProvider.getOptions(this.choice.id).then((options) => { + return this.choiceProvider.getOptions(this.choice.id, {cmId: this.module.id}).then((options) => { let promise; // Check if the user has answered (synced) to allow show results. @@ -294,7 +294,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo return Promise.resolve(); } - return this.choiceProvider.getResults(this.choice.id).then((results) => { + return this.choiceProvider.getResults(this.choice.id, {cmId: this.module.id}).then((results) => { let hasVotes = false; this.data = []; this.labels = []; diff --git a/src/addon/mod/choice/lang/en.json b/src/addon/mod/choice/lang/en.json index e5c48508f..7adee8f2e 100644 --- a/src/addon/mod/choice/lang/en.json +++ b/src/addon/mod/choice/lang/en.json @@ -4,6 +4,7 @@ "errorgetchoice": "Error getting choice data.", "expired": "This activity closed on {{$a}}.", "full": "(Full)", + "limita": "Limit: {{$a}}", "modulenameplural": "Choices", "noresultsviewable": "The results are not currently viewable.", "notopenyet": "This activity is not available until {{$a}}.", @@ -17,6 +18,7 @@ "publishinfonever": "The results of this activity will not be published after you answer.", "removemychoice": "Remove my choice", "responses": "Responses", + "responsesa": "Responses: {{$a}}", "responsesresultgraphdescription": "{{number}}% of the users chose the option: {{text}}.", "responsesresultgraphheader": "Graph display", "resultsnotsynced": "Your last response must be synchronised before it is included in the results.", diff --git a/src/addon/mod/choice/providers/choice.ts b/src/addon/mod/choice/providers/choice.ts index 88808a6ba..3e74cfb66 100644 --- a/src/addon/mod/choice/providers/choice.ts +++ b/src/addon/mod/choice/providers/choice.ts @@ -13,14 +13,15 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModChoiceOfflineProvider } from './offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for choices. @@ -173,34 +174,26 @@ export class AddonModChoiceProvider { /** * Get a choice with key=value. If more than one is found, only the first will be returned. * - * @param siteId Site ID. * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the choice is retrieved. */ - protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache?: boolean, - ignoreCache?: boolean): Promise { + protected getChoiceByDataKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { courseids: [courseId] }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getChoiceDataCacheKey(courseId), - omitExpires: forceCache, - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChoiceProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - return site.read('mod_choice_get_choices_by_courses', params, preSets) .then((response: AddonModChoiceGetChoicesByCoursesResult): any => { @@ -221,14 +214,11 @@ export class AddonModChoiceProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the choice is retrieved. */ - getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean) - : Promise { - return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache); + getChoice(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getChoiceByDataKey(courseId, 'coursemodule', cmId, options); } /** @@ -236,39 +226,33 @@ export class AddonModChoiceProvider { * * @param courseId Course ID. * @param choiceId Choice ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the choice is retrieved. */ - getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean) - : Promise { - return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache); + getChoiceById(courseId: number, choiceId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getChoiceByDataKey(courseId, 'id', choiceId, options); } /** * Get choice options. * * @param choiceId Choice ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with choice options. */ - getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getOptions(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getChoiceOptionsCacheKey(choiceId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChoiceProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - return site.read('mod_choice_get_choice_options', params, preSets) .then((response: AddonModChoiceGetChoiceOptionsResult): any => { @@ -285,24 +269,21 @@ export class AddonModChoiceProvider { * Get choice results. * * @param choiceId Choice ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with choice results. */ - getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getResults(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getChoiceResultsCacheKey(choiceId) + const preSets = { + cacheKey: this.getChoiceOptionsCacheKey(choiceId), + component: AddonModChoiceProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - return site.read('mod_choice_get_choice_results', params, preSets) .then((response: AddonModChoiceGetChoiceResults): any => { diff --git a/src/addon/mod/choice/providers/prefetch-handler.ts b/src/addon/mod/choice/providers/prefetch-handler.ts index 656ab5efc..f319c6044 100644 --- a/src/addon/mod/choice/providers/prefetch-handler.ts +++ b/src/addon/mod/choice/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -79,12 +79,21 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when done. */ protected prefetchChoice(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.choiceProvider.getChoice(courseId, module.id, siteId, false, true).then((choice) => { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + + return this.choiceProvider.getChoice(courseId, module.id, commonOptions).then((choice) => { const promises = []; // Get the options and results. - promises.push(this.choiceProvider.getOptions(choice.id, true, siteId)); - promises.push(this.choiceProvider.getResults(choice.id, true, siteId).then((options) => { + promises.push(this.choiceProvider.getOptions(choice.id, modOptions)); + promises.push(this.choiceProvider.getResults(choice.id, modOptions).then((options) => { // If we can see the users that answered, prefetch their profile and avatar. const subPromises = []; options.forEach((option) => { diff --git a/src/addon/mod/data/components/index/addon-mod-data-index.html b/src/addon/mod/data/components/index/addon-mod-data-index.html index af4a82ad6..c0a92a4ad 100644 --- a/src/addon/mod/data/components/index/addon-mod-data-index.html +++ b/src/addon/mod/data/components/index/addon-mod-data-index.html @@ -12,7 +12,7 @@ - + diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index b63f00ce0..3f79e9972 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -180,69 +180,67 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * @param showErrors If show errors to the user of hide them. * @return Promise resolved when done. */ - protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { let canAdd = false, canSearch = false; - return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { - this.data = data; - this.hasComments = data.comments; + this.data = await this.dataProvider.getDatabase(this.courseId, this.module.id); + this.hasComments = this.data.comments; - this.description = data.intro || data.description; - this.dataRetrieved.emit(data); + this.description = this.data.intro || this.data.description; + this.dataRetrieved.emit(this.data); - if (sync) { + if (sync) { + try { // Try to synchronize the data. - return this.syncActivity(showErrors).catch(() => { - // Ignore errors. - }); + await this.syncActivity(showErrors); + } catch (error) { + // Ignore errors. } - }).then(() => { - return this.dataProvider.getDatabaseAccessInformation(this.data.id); - }).then((accessData) => { - this.access = accessData; + } - if (!accessData.timeavailable) { - const time = this.timeUtils.timestamp(); + this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.data.coursemodule); + this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, this.groupInfo); - this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ? - parseInt(this.data.timeavailablefrom, 10) * 1000 : false; - this.timeAvailableFromReadable = this.timeAvailableFrom ? this.timeUtils.userDate(this.timeAvailableFrom) : false; - this.timeAvailableTo = this.data.timeavailableto && time > this.data.timeavailableto ? - parseInt(this.data.timeavailableto, 10) * 1000 : false; - this.timeAvailableToReadable = this.timeAvailableTo ? this.timeUtils.userDate(this.timeAvailableTo) : false; + this.access = await this.dataProvider.getDatabaseAccessInformation(this.data.id, { + cmId: this.module.id, + groupId: this.selectedGroup || undefined + }); - this.isEmpty = true; - this.groupInfo = null; + if (!this.access.timeavailable) { + const time = this.timeUtils.timestamp(); - return; - } + this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ? + parseInt(this.data.timeavailablefrom, 10) * 1000 : false; + this.timeAvailableFromReadable = this.timeAvailableFrom ? this.timeUtils.userDate(this.timeAvailableFrom) : false; + this.timeAvailableTo = this.data.timeavailableto && time > this.data.timeavailableto ? + parseInt(this.data.timeavailableto, 10) * 1000 : false; + this.timeAvailableToReadable = this.timeAvailableTo ? this.timeUtils.userDate(this.timeAvailableTo) : false; + this.isEmpty = true; + this.groupInfo = null; + } else { canSearch = true; - canAdd = accessData.canaddentry; + canAdd = this.access.canaddentry; + } - return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => { - this.groupInfo = groupInfo; - this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo); - }); - }).then(() => { - return this.dataProvider.getFields(this.data.id).then((fields) => { - if (fields.length == 0) { - canSearch = false; - canAdd = false; - } - this.search.advanced = []; + const fields = await this.dataProvider.getFields(this.data.id, {cmId: this.module.id}); + this.search.advanced = []; - this.fields = this.utils.arrayToObject(fields, 'id'); - this.fieldsArray = this.utils.objectToArray(this.fields); + this.fields = this.utils.arrayToObject(fields, 'id'); + this.fieldsArray = this.utils.objectToArray(this.fields); + if (this.fieldsArray.length == 0) { + canSearch = false; + canAdd = false; + } - return this.fetchEntriesData(); - }); - }).finally(() => { + try { + await this.fetchEntriesData(); + } finally { this.canAdd = canAdd; this.canSearch = canSearch; this.fillContextMenu(refresh); - }); + } } /** @@ -252,15 +250,17 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp */ protected fetchEntriesData(): Promise { - return this.dataProvider.getDatabaseAccessInformation(this.data.id, this.selectedGroup).then((accessData) => { - // Update values for current group. - this.access.canaddentry = accessData.canaddentry; + const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined; + const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined; - const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined; - const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined; - - return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, search, advSearch, - this.search.sortBy, this.search.sortDirection, this.search.page); + return this.dataHelper.fetchEntries(this.data, this.fieldsArray, { + groupId: this.selectedGroup, + search, + advSearch, + sort: Number(this.search.sortBy), + order: this.search.sortDirection, + page: this.search.page, + cmId: this.module.id, }).then((entries) => { const numEntries = entries.entries.length; const numOfflineEntries = entries.offlineEntries.length; @@ -381,18 +381,29 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * @param groupId Group ID. * @return Resolved when new group is selected or rejected if not. */ - setGroup(groupId: number): Promise { + async setGroup(groupId: number): Promise { this.selectedGroup = groupId; this.search.page = 0; - return this.fetchEntriesData().then(() => { + // Only update canAdd if there's any field, otheerwise, canAdd will remain false. + if (this.fieldsArray.length > 0) { + // Update values for current group. + this.access = await this.dataProvider.getDatabaseAccessInformation(this.data.id, { + groupId: this.selectedGroup, + cmId: this.module.id, + }); + + this.canAdd = this.access.canaddentry; + } + + try { + await this.fetchEntriesData(); + // Log activity view for coherence with Moodle web. return this.logView(); - }).catch((message) => { - this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); - - return Promise.reject(null); - }); + } catch (error) { + this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + } } /** diff --git a/src/addon/mod/data/fields/latlong/component/latlong.ts b/src/addon/mod/data/fields/latlong/component/latlong.ts index 38a62df64..f34e13102 100644 --- a/src/addon/mod/data/fields/latlong/component/latlong.ts +++ b/src/addon/mod/data/fields/latlong/component/latlong.ts @@ -14,10 +14,9 @@ import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { Platform } from 'ionic-angular'; -import { Geolocation, GeolocationOptions } from '@ionic-native/geolocation'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; -import { CoreAppProvider } from '@providers/app'; +import { CoreApp, CoreAppProvider } from '@providers/app'; +import { CoreGeolocation, CoreGeolocationError, CoreGeolocationErrorReason } from '@providers/geolocation'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** @@ -33,15 +32,14 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo east: number; showGeolocation: boolean; - constructor(protected fb: FormBuilder, - protected platform: Platform, - protected geolocation: Geolocation, + constructor( + protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected sanitizer: DomSanitizer, - protected appProvider: CoreAppProvider) { + appProvider: CoreAppProvider) { super(fb); - this.showGeolocation = !this.appProvider.isDesktop(); + this.showGeolocation = !appProvider.isDesktop(); } /** @@ -73,7 +71,7 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo const eastFixed = east ? east.toFixed(4) : '0.0000'; let url; - if (this.platform.is('ios')) { + if (CoreApp.instance.isIOS()) { url = 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed; } else { url = 'geo:' + northFixed + ',' + eastFixed; @@ -115,33 +113,51 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo * * @param $event The event. */ - getLocation(event: Event): void { + async getLocation(event: Event): Promise { event.preventDefault(); const modal = this.domUtils.showModalLoading('addon.mod_data.gettinglocation', true); - const options: GeolocationOptions = { - enableHighAccuracy: true, - timeout: 30000 - }; + try { + const coordinates = await CoreGeolocation.instance.getCoordinates(); - this.geolocation.getCurrentPosition(options).then((result) => { - this.form.controls['f_' + this.field.id + '_0'].setValue(result.coords.latitude); - this.form.controls['f_' + this.field.id + '_1'].setValue(result.coords.longitude); - }).catch((error) => { - if (this.isPermissionDeniedError(error)) { - this.domUtils.showErrorModal('addon.mod_data.locationpermissiondenied', true); + this.form.controls['f_' + this.field.id + '_0'].setValue(coordinates.latitude); + this.form.controls['f_' + this.field.id + '_1'].setValue(coordinates.longitude); + } catch (error) { + this.showLocationErrorModal(error); + } - return; - } - - this.domUtils.showErrorModalDefault(error, 'Error getting location'); - }).finally(() => { - modal.dismiss(); - }); + modal.dismiss(); } - protected isPermissionDeniedError(error?: any): boolean { - return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; + /** + * Show the appropriate error modal for the given error getting the location. + * + * @param error Location error. + */ + protected showLocationErrorModal(error: any): void { + if (error instanceof CoreGeolocationError) { + this.domUtils.showErrorModal(this.getGeolocationErrorMessage(error), true); + + return; + } + + this.domUtils.showErrorModalDefault(error, 'Error getting location'); } + + /** + * Get error message from a geolocation error. + * + * @param error Geolocation error. + */ + protected getGeolocationErrorMessage(error: CoreGeolocationError): string { + // tslint:disable-next-line: switch-default + switch (error.reason) { + case CoreGeolocationErrorReason.PermissionDenied: + return 'addon.mod_data.locationpermissiondenied'; + case CoreGeolocationErrorReason.LocationNotEnabled: + return 'addon.mod_data.locationnotenabled'; + } + } + } diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json index de89c6bca..920d6f014 100644 --- a/src/addon/mod/data/lang/en.json +++ b/src/addon/mod/data/lang/en.json @@ -23,10 +23,12 @@ "gettinglocation": "Getting location", "latlongboth": "Both latitude and longitude are required.", "locationpermissiondenied": "Permission to access your location has been denied.", + "locationnotenabled": "Location is not enabled", "menuchoose": "Choose...", "modulenameplural": "Databases", "more": "More", "mylocation": "My location", + "noaccess": "You do not have access to this page", "nomatch": "No matching entries found!", "norecords": "No entries in database", "notapproved": "Entry is not approved yet.", diff --git a/src/addon/mod/data/pages/edit/edit.html b/src/addon/mod/data/pages/edit/edit.html index 7b5418da5..557f9a175 100644 --- a/src/addon/mod/data/pages/edit/edit.html +++ b/src/addon/mod/data/pages/edit/edit.html @@ -18,8 +18,8 @@ -
- +
+
diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index 877785bbd..2a911e615 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -52,6 +52,8 @@ export class AddonModDataEditPage { protected siteId: string; protected offline: boolean; protected forceLeave = false; // To allow leaving the page without checking for changes. + protected initialSelectedGroup = null; + protected isEditing = false; title = ''; component = AddonModDataProvider.COMPONENT; @@ -75,7 +77,10 @@ export class AddonModDataEditPage { this.module = params.get('module') || {}; this.entryId = params.get('entryId') || null; this.courseId = params.get('courseId'); - this.selectedGroup = params.get('group') || 0; + this.selectedGroup = this.entryId ? null : (params.get('group') || 0); + + // If entryId is lower than 0 or null, it is a new entry or an offline entry. + this.isEditing = this.entryId && this.entryId > 0; this.siteId = sitesProvider.getCurrentSiteId(); @@ -88,7 +93,7 @@ export class AddonModDataEditPage { * View loaded. */ ionViewDidLoad(): void { - this.fetchEntryData(); + this.fetchEntryData(true); } /** @@ -103,7 +108,8 @@ export class AddonModDataEditPage { const inputData = this.editForm.value; - const changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents); + let changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents); + changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup); if (changed) { // Show confirmation if some data has been modified. @@ -120,38 +126,78 @@ export class AddonModDataEditPage { /** * Fetch the entry data. * + * @param [refresh] To refresh all downloaded data. * @return Resolved when done. */ - protected fetchEntryData(): Promise { - return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { - this.title = data.name || this.title; - this.data = data; - this.cssClass = 'addon-data-entries-' + data.id; + protected async fetchEntryData(refresh: boolean = false): Promise { + try { + this.data = await this.dataProvider.getDatabase(this.courseId, this.module.id); + this.title = this.data.name || this.title; + this.cssClass = 'addon-data-entries-' + this.data.id; - return this.dataProvider.getDatabaseAccessInformation(data.id); - }).then((accessData) => { - if (this.entryId) { - return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => { - this.groupInfo = groupInfo; - this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo); - }); - } - }).then(() => { - return this.dataProvider.getFields(this.data.id); - }).then((fieldsData) => { - this.fieldsArray = fieldsData; - this.fields = this.utils.arrayToObject(fieldsData, 'id'); + this.fieldsArray = await this.dataProvider.getFields(this.data.id, {cmId: this.module.id}); + this.fields = this.utils.arrayToObject(this.fieldsArray, 'id'); + + const entry = await this.dataHelper.fetchEntry(this.data, this.fieldsArray, this.entryId); - return this.dataHelper.fetchEntry(this.data, fieldsData, this.entryId); - }).then((entry) => { this.entry = entry.entry; + // Load correct group. + this.selectedGroup = this.selectedGroup == null ? this.entry.groupid : this.selectedGroup; + + // Check permissions when adding a new entry or offline entry. + if (!this.isEditing) { + let haveAccess = false; + + if (refresh) { + this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.data.coursemodule); + this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, this.groupInfo); + this.initialSelectedGroup = this.selectedGroup; + } + + if (this.groupInfo.groups.length > 0) { + if (refresh) { + const canAddGroup = {}; + + await Promise.all(this.groupInfo.groups.map(async (group) => { + const accessData = await this.dataProvider.getDatabaseAccessInformation(this.data.id, { + cmId: this.module.id, groupId: group.id}); + + canAddGroup[group.id] = accessData.canaddentry; + })); + + this.groupInfo.groups = this.groupInfo.groups.filter((group) => { + return !!canAddGroup[group.id]; + }); + + haveAccess = canAddGroup[this.selectedGroup]; + } else { + // Groups already filtered, so it have access. + haveAccess = true; + } + } else { + const accessData = await this.dataProvider.getDatabaseAccessInformation(this.data.id, {cmId: this.module.id}); + haveAccess = accessData.canaddentry; + } + + if (!haveAccess) { + // You shall not pass, go back. + this.domUtils.showErrorModal('addon.mod_data.noaccess', true); + + // Go back to entry list. + this.forceLeave = true; + this.navCtrl.pop(); + + return; + } + } + this.editFormRender = this.displayEditFields(); - }).catch((message) => { + } catch (message) { this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); - }).finally(() => { - this.loaded = true; - }); + } + + this.loaded = true; } /** @@ -160,7 +206,7 @@ export class AddonModDataEditPage { * @param e Event. * @return Resolved when done. */ - save(e: Event): Promise { + save(e: Event): Promise { e.preventDefault(); e.stopPropagation(); @@ -169,6 +215,7 @@ export class AddonModDataEditPage { return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents).then((changed) => { + changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup); if (!changed) { if (this.entryId) { return this.returnToEntryList(); @@ -196,7 +243,7 @@ export class AddonModDataEditPage { return Promise.reject(e); }).then((editData) => { if (editData.length > 0) { - if (this.entryId) { + if (this.isEditing) { return this.dataProvider.editEntry(this.data.id, this.entryId, this.courseId, editData, this.fields, undefined, this.offline); } @@ -213,20 +260,20 @@ export class AddonModDataEditPage { } // This is done if entry is updated when editing or creating if not. - if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) { + if ((this.isEditing && result.updated) || (!this.isEditing && result.newentryid)) { this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.siteId); - if (result.sent) { - this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' }); - } - const promises = []; - this.entryId = this.entryId || result.newentryid; + if (result.sent) { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' }); - promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId)); - promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId)); + if (this.isEditing) { + promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId)); + } + promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId)); + } return Promise.all(promises).then(() => { this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, @@ -264,7 +311,7 @@ export class AddonModDataEditPage { * @param groupId Group identifier to set. * @return Resolved when done. */ - setGroup(groupId: number): Promise { + setGroup(groupId: number): Promise { this.selectedGroup = groupId; this.loaded = false; @@ -322,7 +369,7 @@ export class AddonModDataEditPage { * * @return Resolved when done. */ - protected returnToEntryList(): Promise { + protected returnToEntryList(): Promise { const inputData = this.editForm.value; return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index ed8e2a84e..7a10b347a 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -142,13 +142,13 @@ export class AddonModDataEntryPage implements OnDestroy { this.title = data.name || this.title; this.data = data; - return this.dataProvider.getFields(this.data.id).then((fieldsData) => { + return this.dataProvider.getFields(this.data.id, {cmId: this.module.id}).then((fieldsData) => { this.fields = this.utils.arrayToObject(fieldsData, 'id'); this.fieldsArray = fieldsData; }); }).then(() => { return this.setEntryFromOffset().then(() => { - return this.dataProvider.getDatabaseAccessInformation(this.data.id); + return this.dataProvider.getDatabaseAccessInformation(this.data.id, {cmId: this.module.id}); }); }).then((accessData) => { this.access = accessData; @@ -290,8 +290,13 @@ export class AddonModDataEntryPage implements OnDestroy { const perPage = AddonModDataProvider.PER_PAGE; const page = !emptyOffset && this.offset >= 0 ? Math.floor(this.offset / perPage) : 0; - return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, undefined, undefined, '0', 'DESC', - page, perPage).then((entries) => { + return this.dataHelper.fetchEntries(this.data, this.fieldsArray, { + groupId: this.selectedGroup, + sort: 0, + order: 'DESC', + page, + perPage, + }).then((entries) => { const pageEntries = entries.offlineEntries.concat(entries.entries); let pageIndex; // Index of the entry when concatenating offline and online page entries. @@ -321,8 +326,11 @@ export class AddonModDataEntryPage implements OnDestroy { this.nextOffset = null; } else { // Last entry of the page, check if there are more pages. - promise = this.dataProvider.getEntries(this.data.id, this.selectedGroup, '0', 'DESC', page + 1, perPage) - .then((entries) => { + promise = this.dataProvider.getEntries(this.data.id, { + groupId: this.selectedGroup, + page: page + 1, + perPage: perPage, + }).then((entries) => { this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? this.offset + 1 : null; }); } @@ -330,7 +338,7 @@ export class AddonModDataEntryPage implements OnDestroy { return Promise.resolve(promise).then(() => { if (this.entryId > 0) { // Online entry, we need to fetch the the rating info. - return this.dataProvider.getEntry(this.data.id, this.entryId).then((entry) => { + return this.dataProvider.getEntry(this.data.id, this.entryId, {cmId: this.module.id}).then((entry) => { this.ratingInfo = entry.ratinginfo; }); } diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index 481896b28..26d55fbc3 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -23,6 +23,7 @@ import { AddonModDataOfflineProvider } from './offline'; import { AddonModDataFieldsDelegate } from './fields-delegate'; import { CoreRatingInfo } from '@core/rating/providers/rating'; import { CoreSite } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Database entry (online or offline). @@ -116,46 +117,51 @@ export class AddonModDataProvider { * @param forceOffline Force editing entry in offline. * @return Promise resolved when the action is done. */ - addEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], groupId: number = 0, + async addEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], groupId: number = 0, fields: any, siteId?: string, forceOffline: boolean = false): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. - const storeOffline = (): Promise => { - return this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId) - .then((entry) => { - return { - // Return provissional entry Id. - newentryid: entry, - sent: false, - }; - }); + const storeOffline = async (): Promise => { + const entry = await this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId); + + return { + // Return provissional entry Id. + newentryid: entry, + sent: false, + }; }; + // Checks to store offline. if (!this.appProvider.isOnline() || forceOffline) { const notifications = this.checkFields(fields, contents); if (notifications) { - return Promise.resolve({ - fieldnotifications: notifications - }); + return { fieldnotifications: notifications }; } + } + // Remove unnecessary not synced actions. + await this.deleteEntryOfflineAction(dataId, entryId, 'add', siteId); + + // App is offline, store the action. + if (!this.appProvider.isOnline() || forceOffline) { return storeOffline(); } - return this.addEntryOnline(dataId, contents, groupId, siteId).then((result) => { + try { + const result = await this.addEntryOnline(dataId, contents, groupId, siteId); result.sent = true; return result; - }).catch((error) => { + } catch (error) { if (this.utils.isWebServiceError(error)) { // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); + throw error; } // Couldn't connect to server, store in offline. return storeOffline(); - }); + } } /** @@ -192,48 +198,49 @@ export class AddonModDataProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ - approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise { + async approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. - const storeOffline = (): Promise => { + const storeOffline = async (): Promise => { const action = approve ? 'approve' : 'disapprove'; - return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId) - .then(() => { - return { - sent: false, - }; - }); + await this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId); + + return { + sent: false, + }; }; // Get if the opposite action is not synced. const oppositeAction = approve ? 'disapprove' : 'approve'; - return this.dataOffline.getEntry(dataId, entryId, oppositeAction, siteId).then(() => { - // Found. Just delete the action. - return this.dataOffline.deleteEntry(dataId, entryId, oppositeAction, siteId); - }).catch(() => { + const found = await this.deleteEntryOfflineAction(dataId, entryId, oppositeAction, siteId); + if (found) { + // Offline action has been found and deleted. Stop here. + return; + } - if (!this.appProvider.isOnline()) { - // App is offline, store the action. - return storeOffline(); + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + try { + await this.approveEntryOnline(entryId, approve, siteId); + + return { + sent: true, + }; + } catch (error) { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; } - return this.approveEntryOnline(entryId, approve, siteId).then(() => { - return { - sent: true, - }; - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); - } - - // Couldn't connect to server, store in offline. - return storeOffline(); - }); - }); + // Couldn't connect to server, store in offline. + return storeOffline(); + } } /** @@ -297,60 +304,45 @@ export class AddonModDataProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ - deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise { + async deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. - const storeOffline = (): Promise => { - return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId) - .then(() => { - return { - sent: false, - }; - }); + const storeOffline = async (): Promise => { + await this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId); + + return { + sent: false, + }; }; - let justAdded = false; - // Check if the opposite action is not synced and just delete it. - return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { - if (entries && entries.length) { - // Found. Delete other actions first. - const proms = entries.map((entry) => { - if (entry.action == 'add') { - justAdded = true; - } + const addedOffline = await this.deleteEntryOfflineAction(dataId, entryId, 'add', siteId); + if (addedOffline) { + // Offline add action found and deleted. Stop here. + return; + } - return this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId); - }); + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } - return Promise.all(proms); - } - }).then(() => { - if (justAdded) { - // The field was added offline, delete and stop. - return; + try { + await this.deleteEntryOnline(entryId, siteId); + + return { + sent: true, + }; + } catch (error) { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; } - if (!this.appProvider.isOnline()) { - // App is offline, store the action. - return storeOffline(); - } - - return this.deleteEntryOnline(entryId, siteId).then(() => { - return { - sent: true, - }; - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); - } - - // Couldn't connect to server, store in offline. - return storeOffline(); - }); - }); + // Couldn't connect to server, store in offline. + return storeOffline(); + } } /** @@ -370,6 +362,29 @@ export class AddonModDataProvider { }); } + /** + * Delete entry offline action. + * + * @param dataId Database ID. + * @param entryId Entry ID. + * @param action Action name to delete. + * @param siteId Site ID. + * @return Resolved with true if the action has been found and deleted. + */ + protected async deleteEntryOfflineAction(dataId: number, entryId: number, action: string, siteId: string): Promise { + // Get other not not synced actions. + try { + await this.dataOffline.getEntry(dataId, entryId, action, siteId); + + await this.dataOffline.deleteEntry(dataId, entryId, action, siteId); + + return true; + } catch (error) { + // Not found. + return false; + } + } + /** * Updates an existing entry. * @@ -382,82 +397,50 @@ export class AddonModDataProvider { * @param forceOffline Force editing entry in offline. * @return Promise resolved when the action is done. */ - editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, siteId?: string, - forceOffline: boolean = false): Promise { + async editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, + siteId?: string, forceOffline: boolean = false): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. - const storeOffline = (): Promise => { - return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId) - .then(() => { - return { - updated: true, - sent: false, - }; - }); - }; + const storeOffline = async (): Promise => { + await this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId); - let justAdded = false, - groupId; + return { + updated: true, + sent: false, + }; + }; if (!this.appProvider.isOnline() || forceOffline) { const notifications = this.checkFields(fields, contents); if (notifications) { - return Promise.resolve({ - fieldnotifications: notifications - }); + return { fieldnotifications: notifications }; } } - // Get other not not synced actions. - return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { - if (entries && entries.length) { - // Found. Delete add and edit actions first. - const proms = []; - entries.forEach((entry) => { - if (entry.action == 'add') { - justAdded = true; - groupId = entry.groupid; - proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); - } else if (entry.action == 'edit') { - proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); - } - }); + // Remove unnecessary not synced actions. + await this.deleteEntryOfflineAction(dataId, entryId, 'edit', siteId); - return Promise.all(proms); - } - }).then(() => { - if (justAdded) { - // The field was added offline, add again and stop. - return this.addEntry(dataId, entryId, courseId, contents, groupId, fields, siteId, forceOffline) - .then((result) => { - result.updated = true; - result.sent = true; + if (!this.appProvider.isOnline() || forceOffline) { + // App is offline, store the action. + return storeOffline(); + } - return result; - }); + try { + const result = await this.editEntryOnline(entryId, contents, siteId); + result.sent = true; + + return result; + } catch (error) { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; } - if (!this.appProvider.isOnline() || forceOffline) { - // App is offline, store the action. - return storeOffline(); - } - - return this.editEntryOnline(entryId, contents, siteId).then((result) => { - result.sent = true; - - return result; - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); - } - - // Couldn't connect to server, store in offline. - return storeOffline(); - }); - }); - } + // Couldn't connect to server, store in offline. + return storeOffline(); + } +} /** * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. @@ -482,49 +465,34 @@ export class AddonModDataProvider { * Performs the whole fetch of the entries in the database. * * @param dataId Data ID. - * @param groupId Group ID. - * @param sort Sort the records by this field id. See AddonModDataProvider#getEntries for more info. - * @param order The direction of the sorting. See AddonModDataProvider#getEntries for more info. - * @param perPage Records per page to fetch. It has to match with the prefetch. - * Default on AddonModDataProvider.PER_PAGE. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - fetchAllEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', - perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + fetchAllEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); + options.page = 0; - return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId); + return this.fetchEntriesRecursive(dataId, [], options); } /** * Recursive call on fetch all entries. * * @param dataId Data ID. - * @param groupId Group ID. - * @param sort Sort the records by this field id. See AddonModDataProvider#getEntries for more info. - * @param order The direction of the sorting. See AddonModDataProvider#getEntries for more info. - * @param perPage Records per page to fetch. It has to match with the prefetch. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param entries Entries already fetch (just to concatenate them). - * @param page Page of records to return. - * @param siteId Site ID. + * @param options Other options. * @return Promise resolved when done. */ - protected fetchEntriesRecursive(dataId: number, groupId: number, sort: string, order: string, perPage: number, - forceCache: boolean, ignoreCache: boolean, entries: any, page: number, siteId: string): Promise { - return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId) - .then((result) => { + protected fetchEntriesRecursive(dataId: number, entries: any, options: AddonModDataGetEntriesOptions = {}) + : Promise { + return this.getEntries(dataId, options).then((result) => { entries = entries.concat(result.entries); - const canLoadMore = perPage > 0 && ((page + 1) * perPage) < result.totalcount; + const canLoadMore = options.perPage > 0 && ((options.page + 1) * options.perPage) < result.totalcount; if (canLoadMore) { - return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, entries, page + 1, - siteId); + options.page++; + + return this.fetchEntriesRecursive(dataId, entries, options); } return entries; @@ -557,23 +525,21 @@ export class AddonModDataProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the data is retrieved. */ - protected getDatabaseByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false): + protected getDatabaseByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getDatabaseDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - if (forceCache) { - preSets['omitExpires'] = true; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getDatabaseDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModDataProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_databases_by_courses', params, preSets).then((response) => { if (response && response.databases) { @@ -593,12 +559,11 @@ export class AddonModDataProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the data is retrieved. */ - getDatabase(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getDatabaseByKey(courseId, 'coursemodule', cmId, siteId, forceCache); + getDatabase(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getDatabaseByKey(courseId, 'coursemodule', cmId, options); } /** @@ -606,12 +571,11 @@ export class AddonModDataProvider { * * @param courseId Course ID. * @param id Data ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the data is retrieved. */ - getDatabaseById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getDatabaseByKey(courseId, 'id', id, siteId, forceCache); + getDatabaseById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getDatabaseByKey(courseId, 'id', id, options); } /** @@ -639,32 +603,23 @@ export class AddonModDataProvider { * Get access information for a given database. * * @param dataId Data ID. - * @param groupId Group ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the database is retrieved. */ - getDatabaseAccessInformation(dataId: number, groupId?: number, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getDatabaseAccessInformation(dataId: number, options: AddonModDataAccessInfoOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + options.groupId = options.groupId || 0; + const params = { - databaseid: dataId - }, - preSets = { - cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, groupId) - }; - - if (typeof groupId !== 'undefined') { - params['groupid'] = groupId; - } - - if (offline) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + databaseid: dataId, + groupid: options.groupId, + }; + const preSets = { + cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, options.groupId), + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_data_access_information', params, preSets); }); @@ -674,48 +629,34 @@ export class AddonModDataProvider { * Get entries for a specific database and group. * * @param dataId Data ID. - * @param groupId Group ID. - * @param sort Sort the records by this field id, reserved ids are: - * 0: timeadded - * -1: firstname - * -2: lastname - * -3: approved - * -4: timemodified. - * Empty for using the default database setting. - * @param order The direction of the sorting: 'ASC' or 'DESC'. - * Empty for using the default database setting. - * @param page Page of records to return. - * @param perPage Records per page to return. Default on PER_PAGE. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the database is retrieved. */ - getEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', page: number = 0, - perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.sort = options.sort || 0; + options.order = options.order || 'DESC'; + options.page = options.page || 0; + options.perPage = options.perPage || AddonModDataProvider.PER_PAGE; + + return this.sitesProvider.getSite(options.siteId).then((site) => { // Always use sort and order params to improve cache usage (entries are identified by params). const params = { - databaseid: dataId, - returncontents: 1, - page: page, - perpage: perPage, - groupid: groupId, - sort: sort, - order: order - }, - preSets = { - cacheKey: this.getEntriesCacheKey(dataId, groupId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (forceCache) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + databaseid: dataId, + returncontents: 1, + page: options.page, + perpage: options.perPage, + groupid: options.groupId, + sort: options.sort, + order: options.order, + }; + const preSets = { + cacheKey: this.getEntriesCacheKey(dataId, options.groupId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_entries', params, preSets).then((response) => { response.entries.forEach((entry) => { @@ -753,26 +694,23 @@ export class AddonModDataProvider { * * @param dataId Data ID for caching purposes. * @param entryId Entry ID. - * @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the entry is retrieved. */ - getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string): + getEntry(dataId: number, entryId: number, options: CoreCourseCommonModWSOptions = {}): Promise<{entry: AddonModDataEntry, ratinginfo: CoreRatingInfo}> { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - entryid: entryId, - returncontents: 1 - }, - preSets = { - cacheKey: this.getEntryCacheKey(dataId, entryId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + entryid: entryId, + returncontents: 1, + }; + const preSets = { + cacheKey: this.getEntryCacheKey(dataId, entryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_entry', params, preSets).then((response) => { response.entry.contents = this.utils.arrayToObject(response.entry.contents, 'fieldid'); @@ -797,27 +735,21 @@ export class AddonModDataProvider { * Get the list of configured fields for the given database. * * @param dataId Data ID. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the fields are retrieved. */ - getFields(dataId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getFields(dataId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - databaseid: dataId - }, - preSets = { - cacheKey: this.getFieldsCacheKey(dataId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + databaseid: dataId, + }; + const preSets = { + cacheKey: this.getFieldsCacheKey(dataId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_fields', params, preSets).then((response) => { if (response && response.fields) { @@ -993,46 +925,45 @@ export class AddonModDataProvider { * Performs search over a database. * * @param dataId The data instance id. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param search Search text. It will be used if advSearch is not defined. - * @param advSearch Advanced search data. - * @param sort Sort by this field. - * @param order The direction of the sorting. - * @param page Page of records to return. - * @param perPage Records per page to return. Default on AddonModDataProvider.PER_PAGE. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the action is done. */ - searchEntries(dataId: number, groupId: number = 0, search?: string, advSearch?: any, sort?: string, order?: string, - page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + searchEntries(dataId: number, options: AddonModDataSearchEntriesOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.sort = options.sort || 0; + options.order || options.order || 'DESC'; + options.page = options.page || 0; + options.perPage = options.perPage || AddonModDataProvider.PER_PAGE; + options.readingStrategy = options.readingStrategy || CoreSitesReadingStrategy.PreferNetwork; + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - databaseid: dataId, - groupid: groupId, - returncontents: 1, - page: page, - perpage: perPage - }, - preSets = { - getFromCache: false, - saveToCache: true, - emergencyCache: true - }; + databaseid: dataId, + groupid: options.groupId, + returncontents: 1, + page: options.page, + perpage: options.perPage, + }; + const preSets = { + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof sort != 'undefined') { - params['sort'] = sort; + if (typeof options.sort != 'undefined') { + params['sort'] = options.sort; } - if (typeof order !== 'undefined') { - params['order'] = order; + if (typeof options.order !== 'undefined') { + params['order'] = options.order; } - if (typeof search !== 'undefined') { - params['search'] = search; + if (typeof options.search !== 'undefined') { + params['search'] = options.search; } - if (typeof advSearch !== 'undefined') { - params['advsearch'] = advSearch; + if (typeof options.advSearch !== 'undefined') { + params['advsearch'] = options.advSearch; } return site.read('mod_data_search_entries', params, preSets).then((response) => { @@ -1045,3 +976,34 @@ export class AddonModDataProvider { }); } } + +/** + * Options to pass to get access info. + */ +export type AddonModDataAccessInfoOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group Id. +}; + +/** + * Options to pass to get entries. + */ +export type AddonModDataGetEntriesOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group Id. + sort?: number; // Sort the records by this field id, defaults to 0. Reserved ids are: + // 0: timeadded + // -1: firstname + // -2: lastname + // -3: approved + // -4: timemodified + order?: string; // The direction of the sorting: 'ASC' or 'DESC'. Defaults to 'DESC'. + page?: number; // Page of records to return. Defaults to 0. + perPage?: number; // Records per page to return. Defaults to AddonModDataProvider.PER_PAGE. +}; + +/** + * Options to pass to search entries. + */ +export type AddonModDataSearchEntriesOptions = AddonModDataGetEntriesOptions & { + search?: string; // Search text. It will be used if advSearch is not defined. + advSearch?: any; // Advanced search data. +}; diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 2d48ef390..eaf9a2b5d 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -18,12 +18,13 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { AddonModDataFieldsDelegate } from './fields-delegate'; import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; -import { AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields, AddonModDataEntries } from './data'; +import { + AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields, AddonModDataEntries, AddonModDataSearchEntriesOptions +} from './data'; import { CoreRatingInfo } from '@core/rating/providers/rating'; import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; @@ -33,12 +34,19 @@ import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; @Injectable() export class AddonModDataHelperProvider { - constructor(private sitesProvider: CoreSitesProvider, protected dataProvider: AddonModDataProvider, - private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate, - private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider, - private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private utils: CoreUtilsProvider, - private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, - private ratingOffline: CoreRatingOfflineProvider) {} + constructor( + protected sitesProvider: CoreSitesProvider, + protected dataProvider: AddonModDataProvider, + protected translate: TranslateService, + protected fieldsDelegate: AddonModDataFieldsDelegate, + protected dataOffline: AddonModDataOfflineProvider, + protected fileUploaderProvider: CoreFileUploaderProvider, + protected textUtils: CoreTextUtilsProvider, + protected eventsProvider: CoreEventsProvider, + protected domUtils: CoreDomUtilsProvider, + protected courseProvider: CoreCourseProvider, + protected ratingOffline: CoreRatingOfflineProvider + ) {} /** * Returns the record with the offline actions applied. @@ -210,33 +218,21 @@ export class AddonModDataHelperProvider { * * @param data Database object. * @param fields The fields that define the contents. - * @param groupId Group ID. - * @param search Search text. It will be used if advSearch is not defined. - * @param advSearch Advanced search data. - * @param sort Sort the records by this field id, reserved ids are: - * 0: timeadded - * -1: firstname - * -2: lastname - * -3: approved - * -4: timemodified. - * Empty for using the default database setting. - * @param order The direction of the sorting: 'ASC' or 'DESC'. - * Empty for using the default database setting. - * @param page Page of records to return. - * @param perPage Records per page to return. Default on PER_PAGE. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the database is retrieved. */ - fetchEntries(data: any, fields: any[], groupId: number = 0, search?: string, advSearch?: any[], sort: string = '0', - order: string = 'DESC', page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): - Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + fetchEntries(data: any, fields: any[], options: AddonModDataSearchEntriesOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.page = options.page || 0; + + return this.sitesProvider.getSite(options.siteId).then((site) => { const offlineActions = {}; const result: AddonModDataEntries = { entries: [], totalcount: 0, offlineEntries: [] }; + options.siteId = site.id; const offlinePromise = this.dataOffline.getDatabaseEntries(data.id, site.id).then((actions) => { result.hasOfflineActions = !!actions.length; @@ -248,8 +244,8 @@ export class AddonModDataHelperProvider { offlineActions[action.entryid].push(action); // We only display new entries in the first page when not searching. - if (action.action == 'add' && page == 0 && !search && !advSearch && - (!action.groupid || !groupId || action.groupid == groupId)) { + if (action.action == 'add' && options.page == 0 && !options.search && !options.advSearch && + (!action.groupid || !options.groupId || action.groupid == options.groupId)) { result.offlineEntries.push({ id: action.entryid, canmanageentry: true, @@ -275,16 +271,14 @@ export class AddonModDataHelperProvider { }); let fetchPromise: Promise; - if (search || advSearch) { - fetchPromise = this.dataProvider.searchEntries(data.id, groupId, search, advSearch, sort, order, page, perPage, - site.id).then((fetchResult) => { + if (options.search || options.advSearch) { + fetchPromise = this.dataProvider.searchEntries(data.id, options).then((fetchResult) => { result.entries = fetchResult.entries; result.totalcount = fetchResult.totalcount; result.maxcount = fetchResult.maxcount; }); } else { - fetchPromise = this.dataProvider.getEntries(data.id, groupId, sort, order, page, perPage, false, false, site.id) - .then((fetchResult) => { + fetchPromise = this.dataProvider.getEntries(data.id, options).then((fetchResult) => { result.entries = fetchResult.entries; result.totalcount = fetchResult.totalcount; }); @@ -324,7 +318,7 @@ export class AddonModDataHelperProvider { if (entryId > 0) { // Online entry. - promise = this.dataProvider.getEntry(data.id, entryId, false, site.id); + promise = this.dataProvider.getEntry(data.id, entryId, {cmId: data.coursemodule, siteId: site.id}); } else { // Offline entry or new entry. promise = Promise.resolve({ @@ -644,35 +638,44 @@ export class AddonModDataHelperProvider { * @param courseId Course ID. It not defined, it will be fetched. * @param siteId Site ID. If not defined, current site. */ - showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): void { + async showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - this.domUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord').then(() => { - const modal = this.domUtils.showModalLoading(); + let modal; + try { + await this.domUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord'); - return this.getActivityCourseIdIfNotSet(dataId, courseId, siteId).then((courseId) => { - return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId); - }).catch((message) => { + modal = this.domUtils.showModalLoading(); + + try { + if (entryId > 0) { + courseId = await this.getActivityCourseIdIfNotSet(dataId, courseId, siteId); + } + + this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId); + } catch (message) { this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true); - return Promise.reject(null); - }).then(() => { - return this.utils.allPromises([ - this.dataProvider.invalidateEntryData(dataId, entryId, siteId), - this.dataProvider.invalidateEntriesData(dataId, siteId) - ]).catch(() => { - // Ignore errors. - }); - }).then(() => { - this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId, deleted: true}, siteId); + modal && modal.dismiss(); - this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); - }).finally(() => { - modal.dismiss(); - }); - }).catch(() => { + return; + } + + try { + await this.dataProvider.invalidateEntryData(dataId, entryId, siteId); + await this.dataProvider.invalidateEntriesData(dataId, siteId); + } catch (error) { + // Ignore errors. + } + + this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId, deleted: true}, siteId); + + this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); + } catch (error) { // Ignore error, it was already displayed. - }); + } + + modal && modal.dismiss(); } /** diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts index ecdf8927a..a2aded865 100644 --- a/src/addon/mod/data/providers/prefetch-handler.ts +++ b/src/addon/mod/data/providers/prefetch-handler.ts @@ -16,13 +16,13 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; -import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { AddonModDataProvider, AddonModDataEntry } from './data'; import { AddonModDataSyncProvider } from './sync'; @@ -65,16 +65,17 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * * @param dataId Database Id. * @param groups Array of groups in the activity. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. + * @param options Other options. * @return All unique entries. */ - protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { + protected getAllUniqueEntries(dataId: number, groups: any[], options: CoreSitesCommonWSOptions = {}) + : Promise { + const promises = groups.map((group) => { - return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache, - siteId); + return this.dataProvider.fetchAllEntries(dataId, { + groupId: group.id, + ...options, // Include all options. + }); }); return Promise.all(promises).then((responses) => { @@ -96,31 +97,30 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param module Module to get the files. * @param courseId Course ID the module belongs to. * @param omitFail True to always return even if fails. Default false. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. + * @param options Other options. * @return Promise resolved with the info fetched. */ - protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { + protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean, options: CoreCourseCommonModWSOptions = {}) + : Promise { let database, groups = [], entries = [], files = []; - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.cmId = options.cmId || module.id; + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.dataProvider.getDatabase(courseId, module.id, siteId, forceCache).then((data) => { + return this.dataProvider.getDatabase(courseId, module.id, options).then((data) => { files = this.getIntroFilesFromInstance(module, data); database = data; - return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => { + return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, options.siteId).then((groupInfo) => { if (!groupInfo.groups || groupInfo.groups.length == 0) { groupInfo.groups = [{id: 0}]; } groups = groupInfo.groups; - return this.getAllUniqueEntries(database.id, groups, forceCache, ignoreCache, siteId); + return this.getAllUniqueEntries(database.id, groups, options); }); }).then((uniqueEntries) => { entries = uniqueEntries; @@ -229,8 +229,10 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * @return Promise resolved with true if downloadable, resolved with false otherwise. */ isDownloadable(module: any, courseId: number): boolean | Promise { - return this.dataProvider.getDatabase(courseId, module.id, undefined, true).then((database) => { - return this.dataProvider.getDatabaseAccessInformation(database.id).then((accessData) => { + return this.dataProvider.getDatabase(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((database) => { + return this.dataProvider.getDatabaseAccessInformation(database.id, {cmId: module.id}).then((accessData) => { // Check if database is restricted by time. if (!accessData.timeavailable) { const time = this.timeUtils.timestamp(); @@ -281,23 +283,31 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * @return Promise resolved when done. */ protected prefetchDatabase(module: any, courseId: number, single: boolean, siteId: string): Promise { + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; - return this.getDatabaseInfoHelper(module, courseId, false, false, true, siteId).then((info) => { + return this.getDatabaseInfoHelper(module, courseId, false, options).then((info) => { // Prefetch the database data. const database = info.database, commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(), promises = []; - promises.push(this.dataProvider.getFields(database.id, false, true, siteId)); + promises.push(this.dataProvider.getFields(database.id, options)); promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id)); info.groups.forEach((group) => { - promises.push(this.dataProvider.getDatabaseAccessInformation(database.id, group.id, false, true, siteId)); + promises.push(this.dataProvider.getDatabaseAccessInformation(database.id, { + groupId: group.id, + ...options, // Include all options. + })); }); info.entries.forEach((entry) => { - promises.push(this.dataProvider.getEntry(database.id, entry.id, true, siteId)); + promises.push(this.dataProvider.getEntry(database.id, entry.id, options)); if (commentsEnabled && database.comments) { promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id, diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts index 4cf76575d..72995f623 100644 --- a/src/addon/mod/data/providers/sync.ts +++ b/src/addon/mod/data/providers/sync.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -188,7 +188,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { courseId = offlineActions[0].courseid; // Send the answers. - return this.dataProvider.getDatabaseById(courseId, dataId, siteId).then((database) => { + return this.dataProvider.getDatabaseById(courseId, dataId, {siteId}).then((database) => { data = database; const offlineEntries = {}; @@ -208,7 +208,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { }).then(() => { if (result.updated) { // Data has been sent to server. Now invalidate the WS calls. - return this.dataProvider.invalidateContent(data.cmid, courseId, siteId).catch(() => { + return this.dataProvider.invalidateContent(data.coursemodule, courseId, siteId).catch(() => { // Ignore errors. }); } @@ -233,18 +233,23 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { * @return Promise resolved if success, rejected otherwise. */ protected syncEntry(data: any, entryActions: AddonModDataOfflineAction[], result: any, siteId?: string): Promise { - let discardError, - timePromise, - entryId = entryActions[0].entryid, - offlineId, - deleted = false; + let discardError; + let timePromise; + let entryId = entryActions[0].entryid; + let offlineId; + let deleted = false; const editAction = entryActions.find((action) => action.action == 'add' || action.action == 'edit'); const approveAction = entryActions.find((action) => action.action == 'approve' || action.action == 'disapprove'); const deleteAction = entryActions.find((action) => action.action == 'delete'); + const options = { + cmId: data.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; if (entryId > 0) { - timePromise = this.dataProvider.getEntry(data.id, entryId, true, siteId).then((entry) => { + timePromise = this.dataProvider.getEntry(data.id, entryId, options).then((entry) => { return entry.entry.timemodified; }).catch((error) => { if (error && this.utils.isWebServiceError(error)) { @@ -402,7 +407,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { const promises = []; results.forEach((result) => { - promises.push(this.dataProvider.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, siteId) + promises.push(this.dataProvider.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, {siteId}) .then((data) => { const promises = []; diff --git a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html index c7edc940d..d4f10a815 100644 --- a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/feedback/components/index/index.ts b/src/addon/mod/feedback/components/index/index.ts index daa067482..0f58c27d7 100644 --- a/src/addon/mod/feedback/components/index/index.ts +++ b/src/addon/mod/feedback/components/index/index.ts @@ -184,7 +184,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity } }).then(() => { // Check if there are answers stored in offline. - return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id); + return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, {cmId: this.module.id}); }).then((accessData) => { this.access = accessData; this.showTabs = (accessData.canviewreports || accessData.canviewanalysis) && !accessData.isempty; @@ -220,7 +220,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity const promises = []; if (accessData.cancomplete && accessData.cansubmit && accessData.isopen) { - promises.push(this.feedbackProvider.getResumePage(this.feedback.id).then((goPage) => { + promises.push(this.feedbackProvider.getResumePage(this.feedback.id, {cmId: this.module.id}).then((goPage) => { this.goPage = goPage > 0 ? goPage : false; })); } @@ -421,7 +421,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity setGroup(groupId: number): Promise { this.group = groupId; - return this.feedbackProvider.getAnalysis(this.feedback.id, groupId).then((analysis) => { + return this.feedbackProvider.getAnalysis(this.feedback.id, {groupId, cmId: this.module.id}).then((analysis) => { this.feedback.completedCount = analysis.completedcount; this.feedback.itemsCount = analysis.itemscount; diff --git a/src/addon/mod/feedback/pages/attempt/attempt.ts b/src/addon/mod/feedback/pages/attempt/attempt.ts index 4b17573e1..d6ba346e9 100644 --- a/src/addon/mod/feedback/pages/attempt/attempt.ts +++ b/src/addon/mod/feedback/pages/attempt/attempt.ts @@ -65,7 +65,7 @@ export class AddonModFeedbackAttemptPage { return this.feedbackProvider.getFeedbackById(this.courseId, this.feedbackId).then((feedback) => { this.feedback = feedback; - return this.feedbackProvider.getItems(this.feedbackId); + return this.feedbackProvider.getItems(this.feedbackId, {cmId: this.feedback.coursemodule}); }).then((items) => { // Add responses and format items. this.items = items.items.map((item) => { diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts index 8e136401a..601b1a416 100644 --- a/src/addon/mod/feedback/pages/form/form.ts +++ b/src/addon/mod/feedback/pages/form/form.ts @@ -27,7 +27,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; /** * Page that displays feedback form. @@ -141,6 +141,10 @@ export class AddonModFeedbackFormPage implements OnDestroy { */ protected fetchData(): Promise { this.offline = !this.appProvider.isOnline(); + const options = { + cmId: this.module.id, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; return this.feedbackProvider.getFeedback(this.courseId, this.module.id).then((feedbackData) => { this.feedback = feedbackData; @@ -151,8 +155,7 @@ export class AddonModFeedbackFormPage implements OnDestroy { }).then((accessData) => { if (!this.preview && accessData.cansubmit && !accessData.isempty) { return typeof this.currentPage == 'undefined' ? - this.feedbackProvider.getResumePage(this.feedback.id, this.offline, true) : - Promise.resolve(this.currentPage); + this.feedbackProvider.getResumePage(this.feedback.id, options) : Promise.resolve(this.currentPage); } else { this.preview = true; @@ -162,8 +165,9 @@ export class AddonModFeedbackFormPage implements OnDestroy { if (!this.offline && !this.utils.isWebServiceError(error)) { // If it fails, go offline. this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; - return this.feedbackProvider.getResumePage(this.feedback.id, true); + return this.feedbackProvider.getResumePage(this.feedback.id, options); } return Promise.reject(error); @@ -186,12 +190,18 @@ export class AddonModFeedbackFormPage implements OnDestroy { * @return Promise resolved when done. */ protected fetchAccessData(): Promise { - return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, this.offline, true).catch((error) => { + const options = { + cmId: this.module.id, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; + + return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, options).catch((error) => { if (!this.offline && !this.utils.isWebServiceError(error)) { // If it fails, go offline. this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; - return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, true); + return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, options); } return Promise.reject(error); @@ -203,20 +213,25 @@ export class AddonModFeedbackFormPage implements OnDestroy { } protected fetchFeedbackPageData(page: number = 0): Promise { + const options = { + cmId: this.module.id, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; let promise; this.items = []; if (this.preview) { - promise = this.feedbackProvider.getItems(this.feedback.id); + promise = this.feedbackProvider.getItems(this.feedback.id, {cmId: this.module.id}); } else { this.currentPage = page; - promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, this.offline, true).catch((error) => { + promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, options).catch((error) => { if (!this.offline && !this.utils.isWebServiceError(error)) { // If it fails, go offline. this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; - return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, true); + return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, options); } return Promise.reject(error); @@ -262,8 +277,12 @@ export class AddonModFeedbackFormPage implements OnDestroy { return this.feedbackSync.syncFeedback(this.feedback.id).catch(() => { // Ignore errors. }).then(() => { - return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, goPrevious, formHasErrors, - this.courseId).then((response) => { + return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, { + goPrevious, + formHasErrors, + courseId: this.courseId, + cmId: this.module.id, + }).then((response) => { const jumpTo = parseInt(response.jumpto, 10); if (response.completed) { diff --git a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts index aab3cdac3..35425ea36 100644 --- a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts +++ b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts @@ -111,7 +111,11 @@ export class AddonModFeedbackNonRespondentsPage { this.feedbackLoaded = false; } - return this.feedbackHelper.getNonRespondents(this.feedbackId, this.selectedGroup, this.page).then((response) => { + return this.feedbackHelper.getNonRespondents(this.feedbackId, { + groupId: this.selectedGroup, + page: this.page, + cmId: this.moduleId, + }).then((response) => { this.total = response.total; if (this.users.length < response.total) { diff --git a/src/addon/mod/feedback/pages/respondents/respondents.ts b/src/addon/mod/feedback/pages/respondents/respondents.ts index e05ce6c70..ca21c2325 100644 --- a/src/addon/mod/feedback/pages/respondents/respondents.ts +++ b/src/addon/mod/feedback/pages/respondents/respondents.ts @@ -134,7 +134,11 @@ export class AddonModFeedbackRespondentsPage { this.feedbackLoaded = false; } - return this.feedbackHelper.getResponsesAnalysis(this.feedbackId, this.selectedGroup, this.page).then((responses) => { + return this.feedbackHelper.getResponsesAnalysis(this.feedbackId, { + groupId: this.selectedGroup, + page: this.page, + cmId: this.moduleId, + }).then((responses) => { this.responses.total = responses.totalattempts; this.anonResponses.total = responses.totalanonattempts; diff --git a/src/addon/mod/feedback/providers/feedback.ts b/src/addon/mod/feedback/providers/feedback.ts index cdb16569e..19e39fab9 100644 --- a/src/addon/mod/feedback/providers/feedback.ts +++ b/src/addon/mod/feedback/providers/feedback.ts @@ -14,13 +14,14 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreAppProvider } from '@providers/app'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModFeedbackOfflineProvider } from './offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for feedbacks. @@ -35,7 +36,7 @@ export class AddonModFeedbackProvider { static MULTICHOICE_HIDENOSELECT = 'h'; static MULTICHOICERATED_VALUE_SEP = '####'; - protected ROOT_CACHE_KEY = this.ROOT_CACHE_KEY + ''; + protected ROOT_CACHE_KEY = ''; protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, @@ -130,13 +131,11 @@ export class AddonModFeedbackProvider { * * @param feedbackId Feedback ID. * @param items Item to fill the value. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. + * @param options Other options. * @return Resolved with values when done. */ - protected fillValues(feedbackId: number, items: any[], offline: boolean, ignoreCache: boolean, siteId: string): Promise { - return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId).then((valuesArray) => { + protected fillValues(feedbackId: number, items: any[], options: CoreCourseCommonModWSOptions = {}): Promise { + return this.getCurrentValues(feedbackId, options).then((valuesArray) => { const values = {}; valuesArray.forEach((value) => { @@ -152,7 +151,7 @@ export class AddonModFeedbackProvider { // Ignore errors. }).then(() => { // Merge with offline data. - return this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).then((offlineValuesArray) => { + return this.feedbackOffline.getFeedbackResponses(feedbackId, options.siteId).then((offlineValuesArray) => { const offlineValues = {}; // Merge all values into one array. @@ -203,24 +202,22 @@ export class AddonModFeedbackProvider { * Returns all the feedback non respondents users. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @param previous Only for recurrent use. Object with the previous fetched info. * @return Promise resolved when the info is retrieved. */ - getAllNonRespondents(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) - : Promise { + getAllNonRespondents(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - if (typeof previous == 'undefined') { - previous = { - page: 0, - users: [] - }; - } + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); + previous = previous || { + page: 0, + users: [] + }; - return this.getNonRespondents(feedbackId, groupId, previous.page, ignoreCache, siteId).then((response) => { + return this.getNonRespondents(feedbackId, { + page: previous.page, + ...options, // Include all options. + }).then((response) => { if (previous.users.length < response.total) { previous.users = previous.users.concat(response.users); } @@ -229,7 +226,7 @@ export class AddonModFeedbackProvider { // Can load more. previous.page++; - return this.getAllNonRespondents(feedbackId, groupId, ignoreCache, siteId, previous); + return this.getAllNonRespondents(feedbackId, options, previous); } previous.total = response.total; @@ -241,25 +238,23 @@ export class AddonModFeedbackProvider { * Returns all the feedback user responses. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @param previous Only for recurrent use. Object with the previous fetched info. * @return Promise resolved when the info is retrieved. */ - getAllResponsesAnalysis(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) - : Promise { + getAllResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - if (typeof previous == 'undefined') { - previous = { - page: 0, - attempts: [], - anonattempts: [] - }; - } + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); + previous = previous || { + page: 0, + attempts: [], + anonattempts: [] + }; - return this.getResponsesAnalysis(feedbackId, groupId, previous.page, ignoreCache, siteId).then((responses) => { + return this.getResponsesAnalysis(feedbackId, { + page: previous.page, + ...options, // Include all options. + }).then((responses) => { if (previous.anonattempts.length < responses.totalanonattempts) { previous.anonattempts = previous.anonattempts.concat(responses.anonattempts); } @@ -272,7 +267,7 @@ export class AddonModFeedbackProvider { // Can load more. previous.page++; - return this.getAllResponsesAnalysis(feedbackId, groupId, ignoreCache, siteId, previous); + return this.getAllResponsesAnalysis(feedbackId, options, previous); } previous.totalattempts = responses.totalattempts; @@ -286,27 +281,23 @@ export class AddonModFeedbackProvider { * Get analysis information for a given feedback. * * @param feedbackId Feedback ID. - * @param groupId Group ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - getAnalysis(feedbackId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAnalysisDataCacheKey(feedbackId, groupId) - }; + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getAnalysisDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (groupId) { - params['groupid'] = groupId; - } - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; + if (options.groupId) { + params['groupid'] = options.groupId; } return site.read('mod_feedback_get_analysis', params, preSets); @@ -339,22 +330,23 @@ export class AddonModFeedbackProvider { * * @param feedbackId Feedback ID. * @param attemptId Attempt id to find. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @param previous Only for recurrent use. Object with the previous fetched info. * @return Promise resolved when the info is retrieved. */ - getAttempt(feedbackId: number, attemptId: number, ignoreCache?: boolean, siteId?: string, previous?: any): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - if (typeof previous == 'undefined') { - previous = { - page: 0, - attemptsLoaded: 0, - anonAttemptsLoaded: 0 - }; - } + getAttempt(feedbackId: number, attemptId: number, options: CoreCourseCommonModWSOptions = {}, previous?: any): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); + previous = previous || { + page: 0, + attemptsLoaded: 0, + anonAttemptsLoaded: 0 + }; - return this.getResponsesAnalysis(feedbackId, 0, previous.page, ignoreCache, siteId).then((responses) => { + return this.getResponsesAnalysis(feedbackId, { + page: previous.page, + groupId: 0, + ...options, // Include all options. + }).then((responses) => { let attempt; attempt = responses.attempts.find((attempt) => { @@ -385,7 +377,7 @@ export class AddonModFeedbackProvider { // Can load more. Check there. previous.page++; - return this.getAttempt(feedbackId, attemptId, ignoreCache, siteId, previous); + return this.getAttempt(feedbackId, attemptId, options, previous); } // Not found and all loaded. Reject. @@ -407,23 +399,20 @@ export class AddonModFeedbackProvider { * Returns the temporary completion timemodified for the current user. * * @param feedbackId Feedback ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getCurrentCompletedTimeModified(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getCurrentCompletedTimeModified(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_current_completed_tmp', params, preSets).then((response) => { if (response && typeof response.feedback != 'undefined' && typeof response.feedback.timemodified != 'undefined') { @@ -452,26 +441,20 @@ export class AddonModFeedbackProvider { * Returns the temporary responses or responses of the last submission for the current user. * * @param feedbackId Feedback ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getCurrentValues(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getCurrentValues(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets = { - cacheKey: this.getCurrentValuesDataCacheKey(feedbackId) - }; - - if (offline) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getCurrentValuesDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_unfinished_responses', params, preSets).then((response) => { if (!response || typeof response.responses == 'undefined') { @@ -508,27 +491,20 @@ export class AddonModFeedbackProvider { * Get access information for a given feedback. * * @param feedbackId Feedback ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - getFeedbackAccessInformation(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): - Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getFeedbackAccessInformation(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets = { - cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId) - }; - - if (offline) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_feedback_access_information', params, preSets); }); @@ -570,29 +546,22 @@ export class AddonModFeedbackProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean, - ignoreCache?: boolean): Promise { + protected getFeedbackDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getFeedbackCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getFeedbackCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModFeedbackProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_feedbacks_by_courses', params, preSets).then((response) => { if (response && response.feedbacks) { @@ -614,13 +583,11 @@ export class AddonModFeedbackProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { - return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache, ignoreCache); + getFeedback(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, options); } /** @@ -628,37 +595,32 @@ export class AddonModFeedbackProvider { * * @param courseId Course ID. * @param id Feedback ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { - return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache, ignoreCache); + getFeedbackById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getFeedbackDataByKey(courseId, 'id', id, options); } /** * Returns the items (questions) in the given feedback. * * @param feedbackId Feedback ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getItems(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getItems(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getItemsDataCacheKey(feedbackId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getItemsDataCacheKey(feedbackId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_items', params, preSets); }); @@ -678,29 +640,25 @@ export class AddonModFeedbackProvider { * Retrieves a list of students who didn't submit the feedback. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page The page of records to return. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, ignoreCache?: boolean, siteId?: string) - : Promise { + getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.page = options.page || 0; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId, - groupid: groupId, - page: page - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, groupId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + feedbackid: feedbackId, + groupid: options.groupId, + page: options.page, + }; + const preSets = { + cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_non_respondents', params, preSets); }); @@ -751,25 +709,22 @@ export class AddonModFeedbackProvider { * * @param feedbackId Feedback ID. * @param page The page to get. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getPageItemsWithValues(feedbackId: number, page: number, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getPageItemsWithValues(feedbackId: number, page: number, options: CoreCourseCommonModWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.getPageItems(feedbackId, page, siteId).then((response) => { - return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { + return this.getPageItems(feedbackId, page, options.siteId).then((response) => { + return this.fillValues(feedbackId, response.items, options).then((items) => { response.items = items; return response; }); }).catch(() => { // If getPageItems fail we should calculate it using getItems. - return this.getItems(feedbackId, false, siteId).then((response) => { - return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { + return this.getItems(feedbackId, options).then((response) => { + return this.fillValues(feedbackId, response.items, options).then((items) => { // Separate items by pages. let currentPage = 0; const previousPageItems = []; @@ -819,11 +774,17 @@ export class AddonModFeedbackProvider { * @param feedbackId Feedback ID. * @param page Page where we want to jump. * @param changePage If page change is forward (1) or backward (-1). - * @param siteId Site ID. + * @param options Other options. * @return Page number where to jump. Or false if completed or first page. */ - protected getPageJumpTo(feedbackId: number, page: number, changePage: number, siteId: string): Promise { - return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => { + protected getPageJumpTo(feedbackId: number, page: number, changePage: number, options: {cmId?: number, siteId?: string}) + : Promise { + + return this.getPageItemsWithValues(feedbackId, page, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((resp) => { // The page we are going has items. if (resp.items.length > 0) { return page; @@ -831,7 +792,7 @@ export class AddonModFeedbackProvider { // Check we can jump futher. if ((changePage == 1 && resp.hasnextpage) || (changePage == -1 && resp.hasprevpage)) { - return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId); + return this.getPageJumpTo(feedbackId, page + changePage, changePage, options); } // Completed or first page. @@ -843,27 +804,25 @@ export class AddonModFeedbackProvider { * Returns the feedback user responses. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page The page of records to return. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getResponsesAnalysis(feedbackId: number, groupId: number, page: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - feedbackid: feedbackId, - groupid: groupId || 0, - page: page || 0 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, groupId) - }; + getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.page = options.page || 0; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + feedbackid: feedbackId, + groupid: options.groupId, + page: options.page, + }; + const preSets = { + cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_responses_analysis', params, preSets); }); @@ -894,26 +853,20 @@ export class AddonModFeedbackProvider { * Gets the resume page information. * * @param feedbackId Feedback ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getResumePage(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getResumePage(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets = { - cacheKey: this.getResumePageDataCacheKey(feedbackId) - }; - - if (offline) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getResumePageDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_launch_feedback', params, preSets).then((response) => { if (response && typeof response.gopage != 'undefined') { @@ -964,7 +917,7 @@ export class AddonModFeedbackProvider { /** * Invalidate the prefetched content. - * To invalidate files, use AddonFeedbackProvider#invalidateFiles. + * To invalidate files, use AddonModFeedbackProvider#invalidateFiles. * * @param moduleId The module ID. * @param courseId Course ID of the module. @@ -976,7 +929,7 @@ export class AddonModFeedbackProvider { const promises = []; - promises.push(this.getFeedback(courseId, moduleId, siteId).then((feedback) => { + promises.push(this.getFeedback(courseId, moduleId, {siteId}).then((feedback) => { const ps = []; // Do not invalidate module data before getting module info, we need it! @@ -1086,23 +1039,20 @@ export class AddonModFeedbackProvider { * Returns if feedback has been completed * * @param feedbackId Feedback ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - isCompleted(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + isCompleted(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getCompletedDataCacheKey(feedbackId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getCompletedDataCacheKey(feedbackId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModFeedbackProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return this.utils.promiseWorks(site.read('mod_feedback_get_last_completed', params, preSets)); }); @@ -1147,19 +1097,15 @@ export class AddonModFeedbackProvider { * @param feedbackId Feedback ID. * @param page The page being processed. * @param responses The data to be processed the key is the field name (usually type[index]_id). - * @param goPrevious Whether we want to jump to previous page. - * @param formHasErrors Whether the form we sent has required but empty fields (only used in offline). - * @param courseId Course ID the feedback belongs to. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - processPage(feedbackId: number, page: number, responses: any, goPrevious: boolean, formHasErrors: boolean, courseId: number, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + processPage(feedbackId: number, page: number, responses: any, options: AddonModFeedbackProcessPageOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a message to be synchronized later. const storeOffline = (): Promise => { - return this.feedbackOffline.saveResponses(feedbackId, page, responses, courseId, siteId).then(() => { + return this.feedbackOffline.saveResponses(feedbackId, page, responses, options.courseId, options.siteId).then(() => { // Simulate process_page response. const response = { jumpto: page, @@ -1168,11 +1114,11 @@ export class AddonModFeedbackProvider { }; let changePage = 0; - if (goPrevious) { + if (options.goPrevious) { if (page > 0) { changePage = -1; } - } else if (!formHasErrors) { + } else if (!options.formHasErrors) { // We can only go next if it has no errors. changePage = 1; } @@ -1181,7 +1127,11 @@ export class AddonModFeedbackProvider { return response; } - return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => { + return this.getPageItemsWithValues(feedbackId, page, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((resp) => { // Check completion. if (changePage == 1 && !resp.hasnextpage) { response.completed = true; @@ -1189,7 +1139,7 @@ export class AddonModFeedbackProvider { return response; } - return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId).then((loadPage) => { + return this.getPageJumpTo(feedbackId, page + changePage, changePage, options).then((loadPage) => { if (loadPage === false) { // Completed or first page. if (changePage == -1) { @@ -1215,8 +1165,8 @@ export class AddonModFeedbackProvider { } // If there's already a response to be sent to the server, discard it first. - return this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, siteId).then(() => { - return this.processPageOnline(feedbackId, page, responses, goPrevious, siteId).catch((error) => { + return this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, options.siteId).then(() => { + return this.processPageOnline(feedbackId, page, responses, options.goPrevious, options.siteId).catch((error) => { if (this.utils.isWebServiceError(error)) { // The WebService has thrown an error, this means that responses cannot be submitted. return Promise.reject(error); @@ -1252,7 +1202,7 @@ export class AddonModFeedbackProvider { }).then((response) => { // Invalidate and update current values because they will change. return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => { - return this.getCurrentValues(feedbackId, false, false, site.getId()); + return this.getCurrentValues(feedbackId, {siteId: site.getId()}); }).catch(() => { // Ignore errors. }).then(() => { @@ -1262,3 +1212,28 @@ export class AddonModFeedbackProvider { }); } } + +/** + * Common options with a group ID. + */ +export type AddonModFeedbackGroupOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group id, 0 means that the function will determine the user group. Defaults to 0. +}; + +/** + * Common options with a group ID and page. + */ +export type AddonModFeedbackGroupPaginatedOptions = AddonModFeedbackGroupOptions & { + page?: number; // The page of records to return. The page of records to return. +}; + +/** + * Common options with a group ID and page. + */ +export type AddonModFeedbackProcessPageOptions = { + goPrevious?: boolean; // Whether we want to jump to previous page. + formHasErrors?: boolean; // Whether the form we sent has required but empty fields (only used in offline). + cmId?: number; // Module ID. + courseId?: number; // Course ID the feedback belongs to. + siteId?: string; // Site ID. If not defined, current site.; +}; diff --git a/src/addon/mod/feedback/providers/helper.ts b/src/addon/mod/feedback/providers/helper.ts index 2639cb5c6..f136c2456 100644 --- a/src/addon/mod/feedback/providers/helper.ts +++ b/src/addon/mod/feedback/providers/helper.ts @@ -14,11 +14,11 @@ import { Injectable } from '@angular/core'; import { NavController, ViewController } from 'ionic-angular'; -import { AddonModFeedbackProvider } from './feedback'; +import { AddonModFeedbackProvider, AddonModFeedbackGroupPaginatedOptions } from './feedback'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -86,12 +86,11 @@ export class AddonModFeedbackHelperProvider { * Retrieves a list of students who didn't submit the feedback with extra info. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page The page of records to return. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getNonRespondents(feedbackId: number, groupId: number, page: number): Promise { - return this.feedbackProvider.getNonRespondents(feedbackId, groupId, page).then((responses) => { + getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise { + return this.feedbackProvider.getNonRespondents(feedbackId, options).then((responses) => { return this.addImageProfileToAttempts(responses.users).then((users) => { responses.users = users; @@ -186,12 +185,11 @@ export class AddonModFeedbackHelperProvider { * Returns the feedback user responses with extra info. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page The page of records to return. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getResponsesAnalysis(feedbackId: number, groupId: number, page: number): Promise { - return this.feedbackProvider.getResponsesAnalysis(feedbackId, groupId, page).then((responses) => { + getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise { + return this.feedbackProvider.getResponsesAnalysis(feedbackId, options).then((responses) => { return this.addImageProfileToAttempts(responses.attempts).then((attempts) => { responses.attempts = attempts; @@ -227,7 +225,11 @@ export class AddonModFeedbackHelperProvider { return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackRespondentsPage', stateParams, siteId); } - return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, true, siteId).then((attempt) => { + return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, { + cmId: moduleId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((attempt) => { stateParams = { moduleId: module.id, attempt: attempt, diff --git a/src/addon/mod/feedback/providers/prefetch-handler.ts b/src/addon/mod/feedback/providers/prefetch-handler.ts index 0c0562638..375e6a049 100644 --- a/src/addon/mod/feedback/providers/prefetch-handler.ts +++ b/src/addon/mod/feedback/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -143,7 +143,9 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH * @return Promise resolved with true if downloadable, resolved with false otherwise. */ isDownloadable(module: any, courseId: number): boolean | Promise { - return this.feedbackProvider.getFeedback(courseId, module.id, undefined, true).then((feedback) => { + return this.feedbackProvider.getFeedback(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((feedback) => { const now = this.timeUtils.timestamp(); // Check time first if available. @@ -154,7 +156,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH return false; } - return this.feedbackProvider.getFeedbackAccessInformation(feedback.id).then((accessData) => { + return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, {cmId: module.id}).then((accessData) => { return accessData.isopen; }); }); @@ -192,15 +194,24 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH * @return Promise resolved when done. */ protected prefetchFeedback(module: any, courseId: number, single: boolean, siteId: string): Promise { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + // Prefetch the feedback data. - return this.feedbackProvider.getFeedback(courseId, module.id, siteId, false, true).then((feedback) => { + return this.feedbackProvider.getFeedback(courseId, module.id, commonOptions).then((feedback) => { let files = (feedback.pageaftersubmitfiles || []).concat(this.getIntroFilesFromInstance(module, feedback)); - return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, false, true, siteId).then((accessData) => { + return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, modOptions).then((accessData) => { const p2 = []; if (accessData.canedititems || accessData.canviewreports) { // Get all groups analysis. - p2.push(this.feedbackProvider.getAnalysis(feedback.id, undefined, true, siteId)); + p2.push(this.feedbackProvider.getAnalysis(feedback.id, modOptions)); p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId, true) .then((groupInfo) => { const p3 = []; @@ -209,11 +220,16 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH groupInfo.groups = [{id: 0}]; } groupInfo.groups.forEach((group) => { - p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, true, siteId)); - p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, true, siteId)); + const groupOptions = { + groupId: group.id, + ...modOptions, // Include all mod options. + }; + + p3.push(this.feedbackProvider.getAnalysis(feedback.id, groupOptions)); + p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, groupOptions)); if (!accessData.isanonymous) { - p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, group.id, true, siteId)); + p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, groupOptions)); } }); @@ -221,7 +237,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH })); } - p2.push(this.feedbackProvider.getItems(feedback.id, true, siteId).then((response) => { + p2.push(this.feedbackProvider.getItems(feedback.id, commonOptions).then((response) => { response.items.forEach((item) => { files = files.concat(item.itemfiles); }); @@ -234,8 +250,8 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH p2.push(this.feedbackProvider.processPageOnline(feedback.id, 0, {}, undefined, siteId).finally(() => { const p4 = []; - p4.push(this.feedbackProvider.getCurrentValues(feedback.id, false, true, siteId)); - p4.push(this.feedbackProvider.getResumePage(feedback.id, false, true, siteId)); + p4.push(this.feedbackProvider.getCurrentValues(feedback.id, modOptions)); + p4.push(this.feedbackProvider.getResumePage(feedback.id, modOptions)); return Promise.all(p4); })); diff --git a/src/addon/mod/feedback/providers/sync.ts b/src/addon/mod/feedback/providers/sync.ts index f3643f219..f4716e7b5 100644 --- a/src/addon/mod/feedback/providers/sync.ts +++ b/src/addon/mod/feedback/providers/sync.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -192,12 +192,12 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv courseId = responses[0].courseid; - return this.feedbackProvider.getFeedbackById(courseId, feedbackId, siteId).then((feedbackData) => { + return this.feedbackProvider.getFeedbackById(courseId, feedbackId, {siteId}).then((feedbackData) => { feedback = feedbackData; if (!feedback.multiple_submit) { // If it does not admit multiple submits, check if it is completed to know if we can submit. - return this.feedbackProvider.isCompleted(feedbackId); + return this.feedbackProvider.isCompleted(feedbackId, {cmId: feedback.coursemodule, siteId}); } else { return false; } @@ -220,7 +220,10 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv return Promise.all(promises); } - return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, true, siteId).then((timemodified) => { + return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((timemodified) => { // Sort by page. responses.sort((a, b) => { return a.page - b.page; diff --git a/src/addon/mod/folder/components/index/addon-mod-folder-index.html b/src/addon/mod/folder/components/index/addon-mod-folder-index.html index 2aa7976f3..e56091080 100644 --- a/src/addon/mod/folder/components/index/addon-mod-folder-index.html +++ b/src/addon/mod/folder/components/index/addon-mod-folder-index.html @@ -6,7 +6,7 @@ - + diff --git a/src/addon/mod/folder/providers/folder.ts b/src/addon/mod/folder/providers/folder.ts index 1f1281e57..af356293f 100644 --- a/src/addon/mod/folder/providers/folder.ts +++ b/src/addon/mod/folder/providers/folder.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -41,11 +41,11 @@ export class AddonModFolderProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the book is retrieved. */ - getFolder(courseId: number, cmId: number, siteId?: string): Promise { - return this.getFolderByKey(courseId, 'coursemodule', cmId, siteId); + getFolder(courseId: number, cmId: number, options?: CoreSitesCommonWSOptions): Promise { + return this.getFolderByKey(courseId, 'coursemodule', cmId, options); } /** @@ -54,18 +54,21 @@ export class AddonModFolderProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the book is retrieved. */ - protected getFolderByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getFolderByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getFolderCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getFolderCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModFolderProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_folder_get_folders_by_courses', params, preSets) .then((response: AddonModFolderGetFoldersByCoursesResult): any => { diff --git a/src/addon/mod/forum/components/discussion-options-menu/discussion-options-menu.ts b/src/addon/mod/forum/components/discussion-options-menu/discussion-options-menu.ts index 142c25706..754ecff79 100644 --- a/src/addon/mod/forum/components/discussion-options-menu/discussion-options-menu.ts +++ b/src/addon/mod/forum/components/discussion-options-menu/discussion-options-menu.ts @@ -49,7 +49,7 @@ export class AddonForumDiscussionOptionsMenuComponent implements OnInit { ngOnInit(): void { if (this.forumProvider.isSetPinStateAvailableForSite()) { // Use the canAddDiscussion WS to check if the user can pin discussions. - this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => { + this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => { this.canPin = !!response.canpindiscussions; }).catch(() => { this.canPin = false; diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html index 31e15b3e0..4f00d77b0 100644 --- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html +++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/forum/components/index/index.ts b/src/addon/mod/forum/components/index/index.ts index 6795fccac..9642aaddf 100644 --- a/src/addon/mod/forum/components/index/index.ts +++ b/src/addon/mod/forum/components/index/index.ts @@ -133,7 +133,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } if (typeof data.deleted != 'undefined' && data.deleted) { - if (data.post.parent == 0 && this.splitviewCtrl && this.splitviewCtrl.isOn()) { + if (data.post.parentid == 0 && this.splitviewCtrl && this.splitviewCtrl.isOn()) { // Discussion deleted, clear details page. this.splitviewCtrl.emptyDetails(); } @@ -250,7 +250,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom promises.push(this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => { this.usesGroups = (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS); })); - promises.push(this.forumProvider.getAccessInformation(this.forum.id).then((accessInfo) => { + promises.push(this.forumProvider.getAccessInformation(this.forum.id, {cmId: this.module.id}).then((accessInfo) => { // Disallow adding discussions if cut-off date is reached and the user has not the capability to override it. // Just in case the forum was fetched from WS when the cut-off date was not reached but it is now. const cutoffDateReached = this.forumHelper.isCutoffDateReached(this.forum) && !accessInfo.cancanoverridecutoff; @@ -259,7 +259,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (this.forumProvider.isSetPinStateAvailableForSite()) { // Use the canAddDiscussion WS to check if the user can pin discussions. - promises.push(this.forumProvider.canAddDiscussionToAll(this.forum.id).then((response) => { + promises.push(this.forumProvider.canAddDiscussionToAll(this.forum.id, {cmId: this.module.id}).then((response) => { this.canPin = !!response.canpindiscussions; }).catch(() => { this.canPin = false; @@ -354,7 +354,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.page = 0; } - return this.forumProvider.getDiscussions(this.forum.id, this.selectedSortOrder.value, this.page).then((response) => { + return this.forumProvider.getDiscussions(this.forum.id, { + cmId: this.forum.cmid, + sortOrder: this.selectedSortOrder.value, + page: this.page, + }).then((response) => { let promise; if (this.usesGroups) { promise = this.forumProvider.formatDiscussionsGroups(this.forum.cmid, response.discussions); diff --git a/src/addon/mod/forum/components/post-options-menu/addon-forum-post-options-menu.html b/src/addon/mod/forum/components/post-options-menu/addon-forum-post-options-menu.html index 9b7e34dd6..d66529bc8 100644 --- a/src/addon/mod/forum/components/post-options-menu/addon-forum-post-options-menu.html +++ b/src/addon/mod/forum/components/post-options-menu/addon-forum-post-options-menu.html @@ -1,12 +1,12 @@ - +

{{ 'addon.mod_forum.edit' | translate }}

- + -

{{ 'addon.mod_forum.delete' | translate }}

-

{{ 'core.discard' | translate }}

+

{{ 'addon.mod_forum.delete' | translate }}

+

{{ 'core.discard' | translate }}

{{ 'core.numwords' | translate: {'$a': wordCount} }}

diff --git a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts index cd0fb7fdf..8b9f2c69c 100644 --- a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts +++ b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, NgZone } from '@angular/core'; import { NavParams, ViewController } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { AddonModForumProvider } from '../../providers/forum'; +import { CoreApp } from '@providers/app'; +import { Network } from '@ionic-native/network'; /** * This component is meant to display a popover with the post options. @@ -34,43 +36,70 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { canDelete = false; loaded = false; url: string; + isOnline: boolean; + offlinePost: boolean; + + protected cmId: number; + protected onlineObserver: any; constructor(navParams: NavParams, + network: Network, + zone: NgZone, protected viewCtrl: ViewController, protected domUtils: CoreDomUtilsProvider, protected forumProvider: AddonModForumProvider, protected sitesProvider: CoreSitesProvider) { this.post = navParams.get('post'); this.forumId = navParams.get('forumId'); + this.cmId = navParams.get('cmId'); + + this.isOnline = CoreApp.instance.isOnline(); + this.onlineObserver = network.onchange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = CoreApp.instance.isOnline(); + }); + }); } /** * Component being initialized. */ - ngOnInit(): void { - if (this.forumId) { - if (this.post.id) { - const site: CoreSite = this.sitesProvider.getCurrentSite(); - this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussion}, 'p' + this.post.id); - - this.forumProvider.getDiscussionPost(this.forumId, this.post.discussion, this.post.id, true).then((post) => { - this.canDelete = post.capabilities.delete && this.forumProvider.isDeletePostAvailable(); - this.canEdit = post.capabilities.edit && this.forumProvider.isUpdatePostAvailable(); - this.wordCount = post.wordcount; - }).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.'); - }).finally(() => { - this.loaded = true; - }); - } else { - // Offline post, you can edit or discard the post. - this.canEdit = true; - this.canDelete = true; - this.loaded = true; - } + async ngOnInit(): Promise { + if (this.post.id > 0) { + const site: CoreSite = this.sitesProvider.getCurrentSite(); + this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussionid}, 'p' + this.post.id); + this.offlinePost = false; } else { + // Offline post, you can edit or discard the post. this.loaded = true; + this.offlinePost = true; + + return; } + + if (typeof this.post.capabilities.delete == 'undefined') { + if (this.forumId) { + try { + this.post = + await this.forumProvider.getDiscussionPost(this.forumId, this.post.discussionid, this.post.id, { + cmId: this.cmId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }); + } catch (error) { + this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.'); + } + } else { + this.loaded = true; + + return; + } + } + + this.canDelete = this.post.capabilities.delete && this.forumProvider.isDeletePostAvailable(); + this.canEdit = this.post.capabilities.edit && this.forumProvider.isUpdatePostAvailable(); + this.wordCount = this.post.haswordcount && this.post.wordcount; + this.loaded = true; } /** @@ -84,7 +113,7 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { * Delete a post. */ deletePost(): void { - if (this.post.id) { + if (!this.offlinePost) { this.viewCtrl.dismiss({action: 'delete'}); } else { this.viewCtrl.dismiss({action: 'deleteoffline'}); @@ -95,10 +124,17 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { * Edit a post. */ editPost(): void { - if (this.post.id) { + if (!this.offlinePost) { this.viewCtrl.dismiss({action: 'edit'}); } else { this.viewCtrl.dismiss({action: 'editoffline'}); } } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.onlineObserver && this.onlineObserver.unsubscribe(); + } } diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html index 10c1f36fc..6a620c98e 100644 --- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html +++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html @@ -3,11 +3,11 @@

- - + +

- +
- +
{{ 'addon.mod_forum.postisprivatereply' | translate }}
@@ -47,17 +47,17 @@
{{ 'core.tag.tags' | translate }}:
- + - +
- + {{ 'addon.mod_forum.subject' | translate }} @@ -70,13 +70,15 @@ {{ 'addon.mod_forum.privatereply' | translate }} - - - - {{ 'addon.mod_forum.advanced' | translate }} - - - + + + + + {{ 'addon.mod_forum.advanced' | translate }} + + + + diff --git a/src/addon/mod/forum/components/post/post.ts b/src/addon/mod/forum/components/post/post.ts index cf4354222..f9402ece9 100644 --- a/src/addon/mod/forum/components/post/post.ts +++ b/src/addon/mod/forum/components/post/post.ts @@ -43,6 +43,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges @Input() post: any; // Post. @Input() courseId: number; // Post's course ID. @Input() discussionId: number; // Post's' discussion ID. + @Input() discussion?: any; // Post's' discussion, only for starting posts. @Input() component: string; // Component this post belong to. @Input() componentId: number; // Component ID. @Input() replyData: any; // Object with the new post data. Usually shared between posts. @@ -92,16 +93,16 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges * Component being initialized. */ ngOnInit(): void { - this.uniqueId = this.post.id ? 'reply' + this.post.id : 'edit' + this.post.parent; + this.uniqueId = this.post.id > 0 ? 'reply' + this.post.id : 'edit' + this.post.parentid; const reTranslated = this.translate.instant('addon.mod_forum.re'); this.displaySubject = !this.parentSubject || (this.post.subject != this.parentSubject && this.post.subject != `Re: ${this.parentSubject}` && this.post.subject != `${reTranslated} ${this.parentSubject}`); - this.defaultReplySubject = (this.post.subject.startsWith('Re: ') || this.post.subject.startsWith(reTranslated)) - ? this.post.subject : `${reTranslated} ${this.post.subject}`; + this.defaultReplySubject = this.post.replysubject || ((this.post.subject.startsWith('Re: ') || + this.post.subject.startsWith(reTranslated)) ? this.post.subject : `${reTranslated} ${this.post.subject}`); - this.optionsMenuEnabled = !this.post.id || (this.forumProvider.isGetDiscussionPostAvailable() && + this.optionsMenuEnabled = this.post.id < 0 || (this.forumProvider.isGetDiscussionPostAvailable() && (this.forumProvider.isDeletePostAvailable() || this.forumProvider.isUpdatePostAvailable())); } @@ -192,7 +193,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges const popover = this.popoverCtrl.create(AddonForumPostOptionsMenuComponent, { post: this.post, - forumId: this.forum.id + forumId: this.forum.id, + cmId: this.forum.cmid, }); popover.onDidDismiss((data) => { if (data && data.action) { @@ -328,7 +330,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges this.syncId = this.forumSync.getDiscussionSyncId(this.discussionId); this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId); - this.setReplyFormData(this.post.parent, true, this.post.subject, this.post.message, this.post.attachments, + this.setReplyFormData(this.post.parentid, true, this.post.subject, this.post.message, this.post.attachments, this.post.isprivatereply); }).catch(() => { // Cancelled. @@ -460,9 +462,9 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges this.domUtils.showDeleteConfirm().then(() => { const promises = []; - promises.push(this.forumOffline.deleteReply(this.post.parent)); + promises.push(this.forumOffline.deleteReply(this.post.parentid)); if (this.forum.id) { - promises.push(this.forumHelper.deleteReplyStoredFiles(this.forum.id, this.post.parent).catch(() => { + promises.push(this.forumHelper.deleteReplyStoredFiles(this.forum.id, this.post.parentid).catch(() => { // Ignore errors, maybe there are no files. })); } diff --git a/src/addon/mod/forum/pages/discussion/discussion.html b/src/addon/mod/forum/pages/discussion/discussion.html index 2cb0c0e84..bc403cdd9 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.html +++ b/src/addon/mod/forum/pages/discussion/discussion.html @@ -1,6 +1,6 @@ - + @@ -41,14 +41,14 @@ {{ 'addon.mod_forum.discussionlocked' | translate }} -
- +
+
- + @@ -60,7 +60,7 @@ - +
diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts index adf2ebbe2..14d83c207 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.ts +++ b/src/addon/mod/forum/pages/discussion/discussion.ts @@ -52,6 +52,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { forum: any = {}; accessInfo: any = {}; discussion: any; + startingPost: any; posts: any[]; discussionLoaded = false; postSubjects: { [id: string]: string }; @@ -85,6 +86,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { protected forumId: number; protected postId: number; + protected parent: number; protected onlineObserver: any; protected syncObserver: any; protected syncManualObserver: any; @@ -120,6 +122,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { this.discussionId = this.discussion ? this.discussion.discussion : navParams.get('discussionId'); this.trackPosts = navParams.get('trackPosts'); this.postId = navParams.get('postId'); + this.parent = navParams.get('parent'); this.isOnline = this.appProvider.isOnline(); this.onlineObserver = network.onchange().subscribe(() => { @@ -136,47 +139,67 @@ export class AddonModForumDiscussionPage implements OnDestroy { /** * View loaded. */ - ionViewDidLoad(): void { - this.sitesProvider.getCurrentSite().getLocalSiteConfig('AddonModForumDiscussionSort').catch(() => { - this.userProvider.getUserPreference('forum_displaymode').catch(() => { - // Ignore errors. - }).then((value) => { - const sortValue = value && parseInt(value, 10); + async ionViewDidLoad(): Promise { + if (this.parent) { + this.sort = 'nested'; // Force nested order. + } else { + this.sort = await this.getUserSort(); + } - switch (sortValue) { + await this.fetchPosts(true, false, true); + + const scrollTo = this.postId || this.parent; + if (scrollTo) { + // Scroll to the post. + setTimeout(() => { + this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_forum-post-' + scrollTo); + }); + } + } + + /** + * Get sort type configured by the current user. + * + * @return Promise resolved with the sort type. + */ + protected async getUserSort(): Promise { + try { + const value = await this.sitesProvider.getCurrentSite().getLocalSiteConfig('AddonModForumDiscussionSort'); + + return value; + } catch (error) { + try { + const value = await this.userProvider.getUserPreference('forum_displaymode'); + + switch (Number(value)) { case 1: - this.sort = 'flat-oldest'; - break; + return 'flat-oldest'; case -1: - this.sort = 'flat-newest'; - break; + return 'flat-newest'; case 3: - this.sort = 'nested'; - break; + return 'nested'; case 2: // Threaded not implemented. default: // Not set, use default sort. // @TODO add fallback to $CFG->forum_displaymode. } - }); - }).then((value) => { - this.sort = value; - }).finally(() => { - this.fetchPosts(true, false, true).then(() => { - if (this.postId) { - // Scroll to the post. - setTimeout(() => { - this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_forum-post-' + this.postId); - }); - } - }); - }); + } catch (error) { + // Ignore errors. + } + } + + return 'flat-oldest'; } /** * User entered the page that contains the component. */ ionViewDidEnter(): void { + if (this.syncObserver) { + // Already setup. + return; + } + // Refresh data if this discussion is synchronized automatically. this.syncObserver = this.eventsProvider.on(AddonModForumSyncProvider.AUTO_SYNCED, (data) => { if (data.forumId == this.forumId && this.discussionId == data.discussionId @@ -231,7 +254,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { } if (typeof data.deleted != 'undefined' && data.deleted) { - if (data.post.parent == 0) { + if (!data.post.parentid) { if (this.svComponent && this.svComponent.isOn()) { this.svComponent.emptyDetails(); } else { @@ -306,9 +329,11 @@ export class AddonModForumDiscussionPage implements OnDestroy { let ratingInfo; return syncPromise.then(() => { - return this.forumProvider.getDiscussionPosts(this.discussionId).then((response) => { + return this.forumProvider.getDiscussionPosts(this.discussionId, {cmId: this.cmId}).then((response) => { onlinePosts = response.posts; ratingInfo = response.ratinginfo; + this.courseId = response.courseid || this.courseId; + this.forumId = response.forumid || this.forumId; }).then(() => { // Check if there are responses stored in offline. return this.forumOffline.getDiscussionReplies(this.discussionId).then((replies) => { @@ -319,7 +344,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { const posts = {}; onlinePosts.forEach((post) => { posts[post.id] = post; - hasUnreadPosts = hasUnreadPosts || !post.postread; + hasUnreadPosts = hasUnreadPosts || !!post.unread; }); replies.forEach((offlineReply) => { @@ -335,7 +360,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { offlineReplies.push(reply); // Disable reply of the parent. Reply in offline to the same post is not allowed, edit instead. - posts[reply.parent].canreply = false; + posts[reply.parentid].capabilities.reply = false; })); }); @@ -348,18 +373,15 @@ export class AddonModForumDiscussionPage implements OnDestroy { }).then(() => { let posts = offlineReplies.concat(onlinePosts); - const startingPost = this.forumProvider.extractStartingPost(posts); - if (startingPost) { - // Update discussion data from first post. - this.discussion = Object.assign(this.discussion || {}, startingPost); - } + this.startingPost = this.forumProvider.extractStartingPost(posts); // If sort type is nested, normal sorting is disabled and nested posts will be displayed. if (this.sort == 'nested') { // Sort first by creation date to make format tree work. this.forumProvider.sortDiscussionPosts(posts, 'ASC'); - posts = this.utils.formatTree(posts, 'parent', 'id', this.discussion.id); + const rootId = this.startingPost ? this.startingPost.id : (this.discussion ? this.discussion.id : 0); + posts = this.utils.formatTree(posts, 'parentid', 'id', rootId); } else { // Set default reply subject. const direction = this.sort == 'flat-newest' ? 'DESC' : 'ASC'; @@ -381,50 +403,52 @@ export class AddonModForumDiscussionPage implements OnDestroy { const promises = []; - promises.push(this.forumProvider.getAccessInformation(this.forumId).then((accessInfo) => { + promises.push(this.forumProvider.getAccessInformation(this.forumId, {cmId: this.cmId}).then((accessInfo) => { this.accessInfo = accessInfo; // Disallow replying if cut-off date is reached and the user has not the capability to override it. // Just in case the posts were fetched from WS when the cut-off date was not reached but it is now. if (this.forumHelper.isCutoffDateReached(forum) && !accessInfo.cancanoverridecutoff) { posts.forEach((post) => { - post.canreply = false; + post.capabilities.reply = false; }); } })); // The discussion object was not passed as parameter and there is no starting post. Should not happen. if (!this.discussion) { - promises.push(this.loadDiscussion(this.forumId, this.discussionId)); + promises.push(this.loadDiscussion(this.forumId, this.cmId, this.discussionId)); } return Promise.all(promises); }).catch(() => { // Ignore errors. }).then(() => { - - if (!this.discussion) { + if (!this.discussion && !this.startingPost) { // The discussion object was not passed as parameter and there is no starting post. Should not happen. return Promise.reject('Invalid forum discussion.'); } - if (this.discussion.userfullname && this.discussion.parent == 0 && this.forum.type == 'single') { - // Hide author for first post and type single. - this.discussion.userfullname = null; + if (this.startingPost.author && this.forum.type == 'single') { + // Hide author and groups for first post and type single. + this.startingPost.author.fullname = null; + this.startingPost.author.groups = null; + } this.posts = posts; this.ratingInfo = ratingInfo; + this.postSubjects = this.getAllPosts().reduce((postSubjects, post) => { postSubjects[post.id] = post.subject; return postSubjects; - }, { [this.discussion.id]: this.discussion.subject }); + }, { [this.startingPost.id]: this.startingPost.subject }); }); }).then(() => { if (this.forumProvider.isSetPinStateAvailableForSite()) { // Use the canAddDiscussion WS to check if the user can pin discussions. - return this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => { + return this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => { this.canPin = !!response.canpindiscussions; }).catch(() => { this.canPin = false; @@ -462,13 +486,14 @@ export class AddonModForumDiscussionPage implements OnDestroy { * Convenience function to load discussion. * * @param forumId Forum ID. + * @param cmId Forum cmid. * @param discussionId Discussion ID. * @return Promise resolved when done. */ - protected loadDiscussion(forumId: number, discussionId: number): Promise { + protected loadDiscussion(forumId: number, cmId: number, discussionId: number): Promise { // Fetch the discussion if not passed as parameter. if (!this.discussion && forumId) { - return this.forumHelper.getDiscussionById(forumId, discussionId).then((discussion) => { + return this.forumHelper.getDiscussionById(forumId, cmId, discussionId).then((discussion) => { this.discussion = discussion; this.discussionId = this.discussion.discussion; }).catch(() => { @@ -688,6 +713,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { this.ratingOfflineObserver && this.ratingOfflineObserver.off(); this.ratingSyncObserver && this.ratingSyncObserver.off(); this.changeDiscObserver && this.changeDiscObserver.off(); + delete this.syncObserver; } /** @@ -722,5 +748,4 @@ export class AddonModForumDiscussionPage implements OnDestroy { return posts; } - } diff --git a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts index 17ff5888e..bde109ba0 100644 --- a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts +++ b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts @@ -112,6 +112,11 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { * User entered the page that contains the component. */ ionViewDidEnter(): void { + if (this.syncObserver) { + // Already setup. + return; + } + // Refresh data if this discussion is synchronized automatically. this.syncObserver = this.eventsProvider.on(AddonModForumSyncProvider.AUTO_SYNCED, (data) => { if (data.forumId == this.forumId && data.userId == this.sitesProvider.getCurrentSiteUserId()) { @@ -171,7 +176,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { this.newDiscussion.postToAllGroups = false; // Use the canAddDiscussion WS to check if the user can add attachments and pin discussions. - promises.push(this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => { + promises.push(this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => { this.canPin = !!response.canpindiscussions; this.canCreateAttachments = !!response.cancreateattachment; }).catch(() => { @@ -185,7 +190,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { })); // Get access information. - promises.push(this.forumProvider.getAccessInformation(this.forumId).then((accessInfo) => { + promises.push(this.forumProvider.getAccessInformation(this.forumId, {cmId: this.cmId}).then((accessInfo) => { this.accessInfo = accessInfo; })); @@ -260,7 +265,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { */ protected validateVisibleGroups(forumGroups: any[]): Promise { // We first check if the user can post to all the groups. - return this.forumProvider.canAddDiscussionToAll(this.forumId).catch(() => { + return this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).catch(() => { // The call failed, let's assume he can't. return { status: false, @@ -280,7 +285,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { const filtered = []; forumGroups.forEach((group) => { - promises.push(this.forumProvider.canAddDiscussion(this.forumId, group.id).catch(() => { + promises.push(this.forumProvider.canAddDiscussion(this.forumId, group.id, {cmId: this.cmId}).catch(() => { /* The call failed, let's return true so the group is shown. If the user can't post to it an error will be shown when he tries to add the discussion. */ return { @@ -337,7 +342,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { if (check) { // We need to check if the user can add a discussion to all participants. - promise = this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => { + promise = this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => { this.canPin = !!response.canpindiscussions; this.canCreateAttachments = !!response.cancreateattachment; @@ -549,6 +554,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { */ ionViewWillLeave(): void { this.syncObserver && this.syncObserver.off(); + delete this.syncObserver; } /** diff --git a/src/addon/mod/forum/providers/discussion-link-handler.ts b/src/addon/mod/forum/providers/discussion-link-handler.ts index 546e477f0..cb6866dd7 100644 --- a/src/addon/mod/forum/providers/discussion-link-handler.ts +++ b/src/addon/mod/forum/providers/discussion-link-handler.ts @@ -60,6 +60,9 @@ export class AddonModForumDiscussionLinkHandler extends CoreContentLinksHandlerB if (data.postid || params.urlHash) { pageParams.postId = parseInt(data.postid || params.urlHash.replace('p', '')); } + if (params.parent) { + pageParams.parent = parseInt(params.parent); + } this.linkHelper.goInSite(navCtrl, 'AddonModForumDiscussionPage', pageParams, siteId); } diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index 2c9d03cdb..a9d92f53a 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -14,16 +14,18 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreGroupsProvider } from '@providers/groups'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModForumOfflineProvider } from './offline'; import { CoreRatingInfo } from '@core/rating/providers/rating'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; +import { CoreUrlUtils } from '@providers/utils/url'; /** * Service that provides some features for forums. @@ -206,26 +208,29 @@ export class AddonModForumProvider { * * @param forumId Forum ID. * @param groupId Group ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object with the following properties: * - status (boolean) * - canpindiscussions (boolean) * - cancreateattachment (boolean) */ - canAddDiscussion(forumId: number, groupId: number, siteId?: string): Promise { + canAddDiscussion(forumId: number, groupId: number, options: CoreCourseCommonModWSOptions = {}): Promise { const params = { forumid: forumId, - groupid: groupId + groupid: groupId, }; const preSets = { - cacheKey: this.getCanAddDiscussionCacheKey(forumId, groupId) + cacheKey: this.getCanAddDiscussionCacheKey(forumId, groupId), + component: AddonModForumProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { return site.read('mod_forum_can_add_discussion', params, preSets).then((result) => { if (result) { if (typeof result.canpindiscussions == 'undefined') { - // WS doesn't support it yet, default it to false to prevent students from seing the option. + // WS doesn't support it yet, default it to false to prevent students from seeing the option. result.canpindiscussions = false; } if (typeof result.cancreateattachment == 'undefined') { @@ -245,14 +250,14 @@ export class AddonModForumProvider { * Check if a user can post to all groups. * * @param forumId Forum ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object with the following properties: * - status (boolean) * - canpindiscussions (boolean) * - cancreateattachment (boolean) */ - canAddDiscussionToAll(forumId: number, siteId?: string): Promise { - return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, siteId); + canAddDiscussionToAll(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, options); } /** @@ -280,7 +285,7 @@ export class AddonModForumProvider { * @return Starting post or undefined if not found. */ extractStartingPost(posts: any[]): any { - const index = posts.findIndex((post) => post.parent == 0); + const index = posts.findIndex((post) => !post.parentid); return index >= 0 ? posts.splice(index, 1).pop() : undefined; } @@ -305,6 +310,18 @@ export class AddonModForumProvider { return this.sitesProvider.wsAvailableInCurrentSite('mod_forum_get_discussion_post'); } + /** + * Returns whether or not getDiscussionPost WS available or not. + * + * @param site Site. If not defined, current site. + * @return If WS is avalaible. + * @since 3.7 + */ + isGetDiscussionPostsAvailable(site?: CoreSite): boolean { + return site ? site.wsAvailable('mod_forum_get_discussion_posts') : + this.sitesProvider.wsAvailableInCurrentSite('mod_forum_get_discussion_posts'); + } + /** * Returns whether or not deletePost WS available or not. * @@ -370,17 +387,19 @@ export class AddonModForumProvider { * Get all course forums. * * @param courseId Course ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the forums are retrieved. */ - getCourseForums(courseId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] + courseids: [courseId], }; const preSets = { cacheKey: this.getForumDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModForumProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_forum_get_forums_by_courses', params, preSets); @@ -393,24 +412,23 @@ export class AddonModForumProvider { * @param forumId Forum ID. * @param discussionId Discussion ID. * @param postId Post ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the post is retrieved. */ - getDiscussionPost(forumId: number, discussionId: number, postId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - postid: postId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId), - updateFrequency: CoreSite.FREQUENCY_USUALLY - }; + getDiscussionPost(forumId: number, discussionId: number, postId: number, options: CoreCourseCommonModWSOptions = {}) + : Promise { - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + postid: postId, + }; + const preSets = { + cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId), + updateFrequency: CoreSite.FREQUENCY_USUALLY, + component: AddonModForumProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_forum_get_discussion_post', params, preSets).then((response) => { if (response.post) { @@ -427,11 +445,11 @@ export class AddonModForumProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the forum is retrieved. */ - getForum(courseId: number, cmId: number, siteId?: string): Promise { - return this.getCourseForums(courseId, siteId).then((forums) => { + getForum(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getCourseForums(courseId, options).then((forums) => { const forum = forums.find((forum) => forum.cmid == cmId); if (forum) { return forum; @@ -446,11 +464,11 @@ export class AddonModForumProvider { * * @param courseId Course ID. * @param forumId Forum ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the forum is retrieved. */ - getForumById(courseId: number, forumId: number, siteId?: string): Promise { - return this.getCourseForums(courseId, siteId).then((forums) => { + getForumById(courseId: number, forumId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getCourseForums(courseId, options).then((forums) => { const forum = forums.find((forum) => forum.id == forumId); if (forum) { return forum; @@ -464,24 +482,25 @@ export class AddonModForumProvider { * Get access information for a given forum. * * @param forumId Forum ID. - * @param forceCache True to always get the value from cache. false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Object with access information. * @since 3.7 */ - getAccessInformation(forumId: number, forceCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAccessInformation(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { if (!site.wsAvailable('mod_forum_get_forum_access_information')) { // Access information not available for 3.6 or older sites. return Promise.resolve({}); } const params = { - forumid: forumId + forumid: forumId, }; const preSets = { cacheKey: this.getAccessInformationCacheKey(forumId), - omitExpires: forceCache + component: AddonModForumProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_forum_get_forum_access_information', params, preSets); @@ -492,20 +511,91 @@ export class AddonModForumProvider { * Get forum discussion posts. * * @param discussionId Discussion ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with forum posts and rating info. */ - getDiscussionPosts(discussionId: number, siteId?: string): Promise<{posts: any[], ratinginfo?: CoreRatingInfo}> { - const params = { - discussionid: discussionId + getDiscussionPosts(discussionId: number, options: CoreCourseCommonModWSOptions = {}): Promise<{posts: any[], courseid?: number, + forumid?: number, ratinginfo?: CoreRatingInfo}> { + + // Convenience function to translate legacy data to new format. + const translateLegacyPostsFormat = (posts: any[]): any[] => { + return posts.map((post) => { + const newPost = { + id: post.id , + discussionid: post.discussion, + parentid: post.parent, + hasparent: !!post.parent, + author: { + id: post.userid, + fullname: post.userfullname, + urls: { profileimage: post.userpictureurl }, + }, + timecreated: post.created, + subject: post.subject, + message: post.message, + attachments : post.attachments, + capabilities: { + reply: !!post.canreply, + }, + + unread: !post.postread, + isprivatereply: !!post.isprivatereply, + tags: post.tags, + }; + + if (post.groupname) { + newPost.author['groups'] = [{name: post.groupname}]; + } + + return newPost; + }); }; - const preSets = { - cacheKey: this.getDiscussionPostsCacheKey(discussionId) + // For some reason, the new WS doesn't use the tags exporter so it returns a different format than other WebServices. + // Convert the new format to the exporter one so it's the same as in other WebServices. + const translateTagsFormatToLegacy = (posts: any[]): any[] => { + posts.forEach((post) => { + post.tags = post.tags.map((tag) => { + const viewUrl = (tag.urls && tag.urls.view) || ''; + const params = CoreUrlUtils.instance.extractUrlParams(viewUrl); + + return { + id: tag.tagid, + taginstanceid: tag.id, + flag: tag.flag ? 1 : 0, + isstandard: tag.isstandard, + rawname: tag.displayname, + name: tag.displayname, + tagcollid: params.tc ? Number(params.tc) : undefined, + taginstancecontextid: params.from ? Number(params.from) : undefined, + }; + }); + }); + + return posts; }; - return this.sitesProvider.getSite(siteId).then((site) => { - return site.read('mod_forum_get_forum_discussion_posts', params, preSets).then((response) => { + const params = { + discussionid: discussionId, + }; + const preSets = { + cacheKey: this.getDiscussionPostsCacheKey(discussionId), + component: AddonModForumProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return this.sitesProvider.getSite(options.siteId).then((site) => { + const wsName = this.isGetDiscussionPostsAvailable(site) ? 'mod_forum_get_discussion_posts' : + 'mod_forum_get_forum_discussion_posts'; + + return site.read(wsName, params, preSets).then((response) => { if (response) { + + if (wsName == 'mod_forum_get_forum_discussion_posts') { + response.posts = translateLegacyPostsFormat(response.posts); + } else { + response.posts = translateTagsFormatToLegacy(response.posts); + } this.storeUserData(response.posts); return response; @@ -525,8 +615,13 @@ export class AddonModForumProvider { sortDiscussionPosts(posts: any[], direction: string): void { // @todo: Check children when sorting. posts.sort((a, b) => { - a = parseInt(a.created, 10); - b = parseInt(b.created, 10); + a = parseInt(a.timecreated, 10) || 0; + b = parseInt(b.timecreated, 10) || 0; + if (a == 0 || b == 0) { + // Leave 0 at the end. + return b - a; + } + if (direction == 'ASC') { return a - b; } else { @@ -592,32 +687,30 @@ export class AddonModForumProvider { * Get forum discussions. * * @param forumId Forum ID. - * @param sortOrder Sort order. - * @param page Page. - * @param forceCache True to always get the value from cache. false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object with: * - discussions: List of discussions. Note that for every discussion in the list discussion.id is the main post ID but * discussion ID is discussion.discussion. * - canLoadMore: True if there may be more discussions to load. */ - getDiscussions(forumId: number, sortOrder?: number, page: number = 0, forceCache?: boolean, siteId?: string): Promise { - sortOrder = sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC; + getDiscussions(forumId: number, options: AddonModForumGetDiscussionsOptions = {}): Promise { + options.sortOrder = options.sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC; + options.page = options.page || 0; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { let method = 'mod_forum_get_forum_discussions_paginated'; const params: any = { forumid: forumId, - page: page, - perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE + page: options.page, + perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, }; if (site.wsAvailable('mod_forum_get_forum_discussions')) { // Since Moodle 3.7. method = 'mod_forum_get_forum_discussions'; - params.sortorder = sortOrder; + params.sortorder = options.sortOrder; } else { - if (sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { + if (options.sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { params.sortby = 'timemodified'; params.sortdirection = 'DESC'; } else { @@ -625,29 +718,27 @@ export class AddonModForumProvider { return Promise.reject(null); } } - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getDiscussionsListCacheKey(forumId, sortOrder) + + const preSets = { + cacheKey: this.getDiscussionsListCacheKey(forumId, options.sortOrder), + component: AddonModForumProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (forceCache) { - preSets.omitExpires = true; - } return site.read(method, params, preSets).catch((error) => { // Try to get the data from cache stored with the old WS method. if (!this.appProvider.isOnline() && method == 'mod_forum_get_forum_discussion' && - sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { + options.sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { const params = { forumid: forumId, - page: page, + page: options.page, perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, sortby: 'timemodified', sortdirection: 'DESC' }; - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getDiscussionsListCacheKey(forumId, sortOrder), - omitExpires: true - }; + Object.assign(preSets, this.sitesProvider.getReadingStrategyPreSets(CoreSitesReadingStrategy.PreferCache)); return site.read('mod_forum_get_forum_discussions_paginated', params, preSets); } @@ -673,6 +764,7 @@ export class AddonModForumProvider { * If a page fails, the discussions until that page will be returned along with a flag indicating an error occurred. * * @param forumId Forum ID. + * @param cmId Forum cmid. * @param sortOrder Sort order. * @param forceCache True to always get the value from cache, false otherwise. * @param numPages Number of pages to get. If not defined, all pages. @@ -682,17 +774,14 @@ export class AddonModForumProvider { * - discussions: List of discussions. * - error: True if an error occurred, false otherwise. */ - getDiscussionsInPages(forumId: number, sortOrder?: number, forceCache?: boolean, numPages?: number, startPage?: number, - siteId?: string): Promise { - if (typeof numPages == 'undefined') { - numPages = -1; - } - startPage = startPage || 0; + getDiscussionsInPages(forumId: number, options: AddonModForumGetDiscussionsInPagesOptions = {}): Promise { + options.page = options.page || 0; const result = { discussions: [], error: false }; + let numPages = typeof options.numPages == 'undefined' ? -1 : options.numPages; if (!numPages) { return Promise.resolve(result); @@ -700,7 +789,7 @@ export class AddonModForumProvider { const getPage = (page: number): Promise => { // Get page discussions. - return this.getDiscussions(forumId, sortOrder, page, forceCache, siteId).then((response) => { + return this.getDiscussions(forumId, options).then((response) => { result.discussions = result.discussions.concat(response.discussions); numPages--; @@ -717,7 +806,7 @@ export class AddonModForumProvider { }); }; - return getPage(startPage); + return getPage(options.page); } /** @@ -753,7 +842,11 @@ export class AddonModForumProvider { this.getAvailableSortOrders().forEach((sortOrder) => { // We need to get the list of discussions to be able to invalidate their posts. - promises.push(this.getDiscussionsInPages(forum.id, sortOrder.value, true).then((response) => { + promises.push(this.getDiscussionsInPages(forum.id, { + cmId: forum.cmid, + sortOrder: sortOrder.value, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((response) => { // Now invalidate the WS calls. const promises = []; @@ -1045,6 +1138,16 @@ export class AddonModForumProvider { const users = {}; list.forEach((entry) => { + if (entry.author) { + const authorId = parseInt(entry.author.id); + if (!isNaN(authorId) && !users[authorId]) { + users[authorId] = { + id: entry.author.id, + fullname: entry.author.fullname, + profileimageurl: entry.author.urls.profileimage + }; + } + } const userId = parseInt(entry.userid); if (!isNaN(userId) && !users[userId]) { users[userId] = { @@ -1091,3 +1194,18 @@ export class AddonModForumProvider { }); } } + +/** + * Options to pass to get discussions. + */ +export type AddonModForumGetDiscussionsOptions = CoreCourseCommonModWSOptions & { + sortOrder?: number; // Sort order. + page?: number; // Page. Defaults to 0. +}; + +/** + * Options to pass to get discussions in pages. + */ +export type AddonModForumGetDiscussionsInPagesOptions = AddonModForumGetDiscussionsOptions & { + numPages?: number; // Number of pages to get. If not defined, all pages. +}; diff --git a/src/addon/mod/forum/providers/helper.ts b/src/addon/mod/forum/providers/helper.ts index 99ca1b487..63aa04019 100644 --- a/src/addon/mod/forum/providers/helper.ts +++ b/src/addon/mod/forum/providers/helper.ts @@ -161,24 +161,23 @@ export class AddonModForumHelperProvider { */ convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise { const reply: any = { - attachments: [], - canreply: false, - children: [], - created: offlineReply.timecreated, - discussion: offlineReply.discussionid, - id: false, - mailed: 0, - mailnow: 0, - message: offlineReply.message, - messageformat: 1, - messagetrust: 0, - modified: false, - parent: offlineReply.postid, - postread: false, + id: -offlineReply.timecreated, + discussionid: offlineReply.discussionid, + parentid: offlineReply.postid, + hasparent: !!offlineReply.postid, + author: { + id: offlineReply.userid, + }, + timecreated: false, subject: offlineReply.subject, - totalscore: 0, - userid: offlineReply.userid, - isprivatereply: offlineReply.options && offlineReply.options.private + message: offlineReply.message, + attachments: [], + capabilities: { + reply: false, + }, + unread: false, + isprivatereply: offlineReply.options && offlineReply.options.private, + tags: null }, promises = []; @@ -187,7 +186,7 @@ export class AddonModForumHelperProvider { reply.attachments = offlineReply.options.attachmentsid.online || []; if (offlineReply.options.attachmentsid.offline) { - promises.push(this.getReplyStoredFiles(offlineReply.forumid, reply.parent, siteId, reply.userid) + promises.push(this.getReplyStoredFiles(offlineReply.forumid, reply.parentid, siteId, offlineReply.userid) .then((files) => { reply.attachments = reply.attachments.concat(files); })); @@ -196,8 +195,8 @@ export class AddonModForumHelperProvider { // Get user data. promises.push(this.userProvider.getProfile(offlineReply.userid, offlineReply.courseid, true).then((user) => { - reply.userfullname = user.fullname; - reply.userpictureurl = user.profileimageurl; + reply.author.fullname = user.fullname; + reply.author.urls = { profileimage: user.profileimageurl }; }).catch(() => { // Ignore errors. })); @@ -270,15 +269,20 @@ export class AddonModForumHelperProvider { * This function is inefficient because it needs to fetch all discussion pages in the worst case. * * @param forumId Forum ID. + * @param cmId Forum cmid * @param discussionId Discussion ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the discussion data. */ - getDiscussionById(forumId: number, discussionId: number, siteId?: string): Promise { + getDiscussionById(forumId: number, cmId: number, discussionId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const findDiscussion = (page: number): Promise => { - return this.forumProvider.getDiscussions(forumId, undefined, page, false, siteId).then((response) => { + return this.forumProvider.getDiscussions(forumId, { + cmId, + page, + siteId, + }).then((response) => { if (response.discussions && response.discussions.length > 0) { // Note that discussion.id is the main post ID but discussion ID is discussion.discussion. const discussion = response.discussions.find((discussion) => discussion.discussion == discussionId); diff --git a/src/addon/mod/forum/providers/module-handler.ts b/src/addon/mod/forum/providers/module-handler.ts index 67839c7bc..f79aa5a75 100644 --- a/src/addon/mod/forum/providers/module-handler.ts +++ b/src/addon/mod/forum/providers/module-handler.ts @@ -143,7 +143,7 @@ export class AddonModForumModuleHandler implements CoreCourseModuleHandler { this.forumProvider.invalidateForumData(courseId).finally(() => { // Handle unread posts. - this.forumProvider.getForum(courseId, moduleId, siteId).then((forumData) => { + this.forumProvider.getForum(courseId, moduleId, {siteId}).then((forumData) => { data.extraBadgeColor = ''; data.extraBadge = forumData.unreadpostscount ? this.translate.instant('addon.mod_forum.unreadpostsnumber', {$a : forumData.unreadpostscount }) : ''; diff --git a/src/addon/mod/forum/providers/prefetch-handler.ts b/src/addon/mod/forum/providers/prefetch-handler.ts index 00af16f83..fd09775d1 100644 --- a/src/addon/mod/forum/providers/prefetch-handler.ts +++ b/src/addon/mod/forum/providers/prefetch-handler.ts @@ -16,10 +16,10 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreGroupsProvider } from '@providers/groups'; @@ -69,7 +69,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand const files = this.getIntroFilesFromInstance(module, forum); // Get posts. - return this.getPostsForPrefetch(forum).then((posts) => { + return this.getPostsForPrefetch(forum, {cmId: module.id}).then((posts) => { // Add posts attachments and embedded files. return files.concat(this.getPostsFiles(posts)); }); @@ -108,13 +108,19 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand * Get the posts to be prefetched. * * @param forum Forum instance. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with array of posts. */ - protected getPostsForPrefetch(forum: any, siteId?: string): Promise { + protected getPostsForPrefetch(forum: any, options: CoreCourseCommonModWSOptions = {}): Promise { const promises = this.forumProvider.getAvailableSortOrders().map((sortOrder) => { // Get discussions in first 2 pages. - return this.forumProvider.getDiscussionsInPages(forum.id, sortOrder.value, false, 2, 0, siteId).then((response) => { + const discussionsOptions = { + sortOrder: sortOrder.value, + numPages: 2, + ...options, // Include all options. + }; + + return this.forumProvider.getDiscussionsInPages(forum.id, discussionsOptions).then((response) => { if (response.error) { return Promise.reject(null); } @@ -122,7 +128,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand const promises = []; response.discussions.forEach((discussion) => { - promises.push(this.forumProvider.getDiscussionPosts(discussion.discussion, siteId)); + promises.push(this.forumProvider.getDiscussionPosts(discussion.discussion, options)); }); return Promise.all(promises); @@ -201,12 +207,21 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Promise resolved when done. */ protected prefetchForum(module: any, courseId: number, single: boolean, siteId: string): Promise { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + // Get the forum data. - return this.forumProvider.getForum(courseId, module.id, siteId).then((forum) => { + return this.forumProvider.getForum(courseId, module.id, commonOptions).then((forum) => { const promises = []; // Prefetch the posts. - promises.push(this.getPostsForPrefetch(forum, siteId).then((posts) => { + promises.push(this.getPostsForPrefetch(forum, modOptions).then((posts) => { const promises = []; const files = this.getIntroFilesFromInstance(module, forum).concat(this.getPostsFiles(posts)); @@ -222,7 +237,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand })); // Prefetch access information. - promises.push(this.forumProvider.getAccessInformation(forum.id, false, siteId)); + promises.push(this.forumProvider.getAccessInformation(forum.id, modOptions)); // Prefetch sort order preference. if (this.forumProvider.isDiscussionListSortingAvailable()) { @@ -243,11 +258,16 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Promise resolved when group data has been prefetched. */ protected prefetchGroupsInfo(forum: any, courseId: number, canCreateDiscussions: boolean, siteId?: string): any { + const options = { + cmId: forum.cmid, + siteId, + }; + // Check group mode. return this.groupsProvider.getActivityGroupMode(forum.cmid, siteId).then((mode) => { if (mode !== CoreGroupsProvider.SEPARATEGROUPS && mode !== CoreGroupsProvider.VISIBLEGROUPS) { // Activity doesn't use groups. Prefetch canAddDiscussionToAll to determine if user can pin/attach. - return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => { + return this.forumProvider.canAddDiscussionToAll(forum.id, options).catch(() => { // Ignore errors. }); } @@ -256,14 +276,14 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand return this.groupsProvider.getActivityAllowedGroups(forum.cmid, undefined, siteId).then((result) => { if (mode === CoreGroupsProvider.SEPARATEGROUPS) { // Groups are already filtered by WS. Prefetch canAddDiscussionToAll to determine if user can pin/attach. - return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => { + return this.forumProvider.canAddDiscussionToAll(forum.id, options).catch(() => { // Ignore errors. }); } if (canCreateDiscussions) { // Prefetch data to check the visible groups when creating discussions. - return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => { + return this.forumProvider.canAddDiscussionToAll(forum.id, options).catch(() => { // The call failed, let's assume he can't. return { status: false @@ -277,7 +297,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand // The user can't post to all groups, let's check which groups he can post to. const groupPromises = []; result.groups.forEach((group) => { - groupPromises.push(this.forumProvider.canAddDiscussion(forum.id, group.id, siteId).catch(() => { + groupPromises.push(this.forumProvider.canAddDiscussion(forum.id, group.id, options).catch(() => { // Ignore errors. })); }); diff --git a/src/addon/mod/forum/providers/sync.ts b/src/addon/mod/forum/providers/sync.ts index 2b204df72..9ca5c5a18 100644 --- a/src/addon/mod/forum/providers/sync.ts +++ b/src/addon/mod/forum/providers/sync.ts @@ -227,7 +227,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider { let groupsPromise; if (data.groupid == AddonModForumProvider.ALL_GROUPS) { // Fetch all group ids. - groupsPromise = this.forumProvider.getForumById(data.courseid, data.forumid, siteId).then((forum) => { + groupsPromise = this.forumProvider.getForumById(data.courseid, data.forumid, {siteId}).then((forum) => { return this.groupsProvider.getActivityAllowedGroups(forum.cmid).then((result) => { return result.groups.map((group) => group.id); }); @@ -330,7 +330,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider { } if (result.warnings.length) { // Fetch forum to construct the warning message. - promises.push(this.forumProvider.getForum(result.itemSet.courseId, result.itemSet.instanceId, siteId) + promises.push(this.forumProvider.getForum(result.itemSet.courseId, result.itemSet.instanceId, {siteId}) .then((forum) => { result.warnings.forEach((warning) => { warnings.push(this.translate.instant('core.warningofflinedatadeleted', { diff --git a/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html index 0c01a1288..475d5e969 100644 --- a/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html @@ -14,7 +14,7 @@ - + diff --git a/src/addon/mod/glossary/components/index/index.ts b/src/addon/mod/glossary/components/index/index.ts index 41bfbd760..3b759ea5e 100644 --- a/src/addon/mod/glossary/components/index/index.ts +++ b/src/addon/mod/glossary/components/index/index.ts @@ -181,10 +181,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity return Promise.resolve({entries: [], count: 0}); } - const limitFrom = append ? this.entries.length : 0; - const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES; - - return this.glossaryProvider.fetchEntries(this.fetchFunction, this.fetchArguments, limitFrom, limitNum).then((result) => { + return this.glossaryProvider.fetchEntries(this.fetchFunction, this.fetchArguments, { + from: append ? this.entries.length : 0, + cmId: this.module.id, + }).then((result) => { if (append) { Array.prototype.push.apply(this.entries, result.entries); } else { diff --git a/src/addon/mod/glossary/pages/edit/edit.ts b/src/addon/mod/glossary/pages/edit/edit.ts index 9bc2ee645..498568084 100644 --- a/src/addon/mod/glossary/pages/edit/edit.ts +++ b/src/addon/mod/glossary/pages/edit/edit.ts @@ -125,7 +125,9 @@ export class AddonModGlossaryEditPage implements OnInit { this.definitionControl.setValue(this.entry.definition); Promise.resolve(promise).then(() => { - this.glossaryProvider.getAllCategories(this.glossary.id).then((categories) => { + this.glossaryProvider.getAllCategories(this.glossary.id, { + cmId: this.module.id, + }).then((categories) => { this.categories = categories; }).finally(() => { this.loaded = true; @@ -215,8 +217,10 @@ export class AddonModGlossaryEditPage implements OnInit { let promise; if (this.entry && !this.glossary.allowduplicatedentries) { // Check if the entry is duplicated in online or offline mode. - promise = this.glossaryProvider.isConceptUsed(this.glossary.id, this.entry.concept, this.entry.timecreated) - .then((used) => { + promise = this.glossaryProvider.isConceptUsed(this.glossary.id, this.entry.concept, { + timeCreated: this.entry.timecreated, + cmId: this.module.id, + }).then((used) => { if (used) { // There's a entry with same name, reject with error message. return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists')); @@ -237,7 +241,12 @@ export class AddonModGlossaryEditPage implements OnInit { // Try to send it to server. // Don't allow offline if there are attachments since they were uploaded fine. return this.glossaryProvider.addEntry(this.glossary.id, this.entry.concept, definition, this.courseId, options, - attach, timecreated, undefined, this.entry, !this.attachments.length, !this.glossary.allowduplicatedentries); + attach, { + timeCreated: timecreated, + discardEntry: this.entry, + allowOffline: !this.attachments.length, + checkDuplicates: !this.glossary.allowduplicatedentries, + }); } }).then((entryId) => { // Delete the local files from the tmp folder. diff --git a/src/addon/mod/glossary/providers/entry-link-handler.ts b/src/addon/mod/glossary/providers/entry-link-handler.ts index 98ec8a6e3..4c7d5a6fd 100644 --- a/src/addon/mod/glossary/providers/entry-link-handler.ts +++ b/src/addon/mod/glossary/providers/entry-link-handler.ts @@ -63,7 +63,7 @@ export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBas if (courseId) { promise = Promise.resolve(courseId); } else { - promise = this.glossaryProvider.getEntry(entryId, siteId).catch((error) => { + promise = this.glossaryProvider.getEntry(entryId, {siteId}).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); return Promise.reject(null); diff --git a/src/addon/mod/glossary/providers/glossary.ts b/src/addon/mod/glossary/providers/glossary.ts index d80820756..79f9887ad 100644 --- a/src/addon/mod/glossary/providers/glossary.ts +++ b/src/addon/mod/glossary/providers/glossary.ts @@ -17,12 +17,13 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreSite } from '@classes/site'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreRatingInfo } from '@core/rating/providers/rating'; import { AddonModGlossaryOfflineProvider } from './offline'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for glossaries. @@ -92,17 +93,19 @@ export class AddonModGlossaryProvider { * Get all the glossaries in a course. * * @param courseId Course Id. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the glossaries. */ - getCourseGlossaries(courseId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getCourseGlossaries(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] + courseids: [courseId], }; const preSets = { cacheKey: this.getCourseGlossariesCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModGlossaryProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_glossaries_by_courses', params, preSets).then((result) => { @@ -146,29 +149,27 @@ export class AddonModGlossaryProvider { * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. * @param field Search and order using: FIRSTNAME or LASTNAME * @param sort The direction of the order: ASC or DESC - * @param from Start returning records from here. - * @param limit Number of records to return. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the entries. */ - getEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, from: number, limit: number, - omitExpires: boolean, forceOffline: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, + options: AddonModGlossaryGetEntriesOptions = {}): Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, letter: letter, field: field, sort: sort, - from: from, - limit: limit + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets = { cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_author', params, preSets); @@ -199,28 +200,24 @@ export class AddonModGlossaryProvider { * @param glossaryId Glossary Id. * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or * constant SHOW_NOT_CATEGORISED for uncategorised entries. - * @param from Start returning records from here. - * @param limit Number of records to return. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the entries. */ - getEntriesByCategory(glossaryId: number, categoryId: number, from: number, limit: number, omitExpires: boolean, - forceOffline: boolean, siteId?: string): Promise { + getEntriesByCategory(glossaryId: number, categoryId: number, options: AddonModGlossaryGetEntriesOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, categoryid: categoryId, - from: from, - limit: limit + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets = { cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_category', params, preSets); @@ -274,29 +271,26 @@ export class AddonModGlossaryProvider { * @param glossaryId Glossary Id. * @param order The way to order the records. * @param sort The direction of the order. - * @param from Start returning records from here. - * @param limit Number of records to return. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the entries. */ - getEntriesByDate(glossaryId: number, order: string, sort: string, from: number, limit: number, omitExpires: boolean, - forceOffline: boolean, siteId?: string): Promise { + getEntriesByDate(glossaryId: number, order: string, sort: string, options: AddonModGlossaryGetEntriesOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, order: order, sort: sort, - from: from, - limit: limit + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets = { cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_date', params, preSets); @@ -336,35 +330,33 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary Id. * @param letter A letter, or a special keyword. - * @param from Start returning records from here. - * @param limit Number of records to return. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the entries. */ - getEntriesByLetter(glossaryId: number, letter: string, from: number, limit: number, omitExpires: boolean, forceOffline: boolean, - siteId?: string): Promise { + getEntriesByLetter(glossaryId: number, letter: string, options: AddonModGlossaryGetEntriesOptions = {}): Promise { + options.from = options.from || 0; + options.limit = options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, letter: letter, - from: from, - limit: limit + from: options.from, + limit: options.limit, }; const preSets = { cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_letter', params, preSets).then((result) => { - if (limit == AddonModGlossaryProvider.LIMIT_ENTRIES) { + if (options.limit == AddonModGlossaryProvider.LIMIT_ENTRIES) { // Store entries in background, don't block the user for this. - this.storeEntries(glossaryId, result.entries, from, site.getId()).catch(() => { + this.storeEntries(glossaryId, result.entries, options.from, site.getId()).catch(() => { // Ignore errors. }); } @@ -420,23 +412,25 @@ export class AddonModGlossaryProvider { * @param siteId Site ID. If not defined, current site. * @return Resolved with the entries. */ - getEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, from: number, - limit: number, omitExpires: boolean, forceOffline: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, + options: AddonModGlossaryGetEntriesOptions = {}): Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, query: query, fullsearch: fullSearch, order: order, sort: sort, - from: from, - limit: limit + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets = { cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_search', params, preSets); @@ -477,12 +471,12 @@ export class AddonModGlossaryProvider { * Get all the categories related to the glossary. * * @param glossaryId Glossary Id. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the categories if supported or empty array if not. */ - getAllCategories(glossaryId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - return this.getCategories(glossaryId, 0, AddonModGlossaryProvider.LIMIT_CATEGORIES, [], site); + getAllCategories(glossaryId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + return this.getCategories(glossaryId, [], site, options); }); } @@ -490,30 +484,37 @@ export class AddonModGlossaryProvider { * Get the categories related to the glossary by sections. It's a recursive function see initial call values. * * @param glossaryId Glossary Id. - * @param from Number of categories already fetched, so fetch will be done from this number. Initial value 0. - * @param limit Number of categories to fetch. Initial value LIMIT_CATEGORIES. - * @param categories Already fetched categories where to append the fetch. Initial value []. + * @param categories Already fetched categories where to append the fetch. * @param site Site object. + * @param options Other options. * @return Promise resolved with the categories. */ - protected getCategories(glossaryId: number, from: number, limit: number, categories: any[], site: CoreSite): Promise { + protected getCategories(glossaryId: number, categories: any[], site: CoreSite, + options: AddonModGlossaryGetCategoriesOptions = {}): Promise { + + options.from = options.from || 0; + options.limit = options.limit || AddonModGlossaryProvider.LIMIT_CATEGORIES; + const params = { id: glossaryId, - from: from, - limit: limit + from: options.from, + limit: options.limit, }; const preSets = { cacheKey: this.getCategoriesCacheKey(glossaryId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_categories', params, preSets).then((response) => { categories = categories.concat(response.categories); - const canLoadMore = (from + limit) < response.count; + const canLoadMore = (options.from + options.limit) < response.count; if (canLoadMore) { - from += limit; + options.from += options.limit; - return this.getCategories(glossaryId, from, limit, categories, site); + return this.getCategories(glossaryId, categories, site, options); } return categories; @@ -547,17 +548,22 @@ export class AddonModGlossaryProvider { * Get one entry by ID. * * @param entryId Entry ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the entry. */ - getEntry(entryId: number, siteId?: string): Promise<{entry: any, ratinginfo: CoreRatingInfo, from?: number}> { - return this.sitesProvider.getSite(siteId).then((site) => { + getEntry(entryId: number, options: CoreCourseCommonModWSOptions = {}) + : Promise<{entry: any, ratinginfo: CoreRatingInfo, from?: number}> { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - id: entryId + id: entryId, }; const preSets = { cacheKey: this.getEntryCacheKey(entryId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entry_by_id', params, preSets).then((response) => { @@ -572,8 +578,12 @@ export class AddonModGlossaryProvider { const searchEntry = (from: number, loadNext: boolean): Promise => { // Get the entries from this "page" and check if the entry we're looking for is in it. - return this.getEntriesByLetter(glossaryId, 'ALL', from, AddonModGlossaryProvider.LIMIT_ENTRIES, false, true, - siteId).then((result) => { + return this.getEntriesByLetter(glossaryId, 'ALL', { + from: from, + readingStrategy: CoreSitesReadingStrategy.OnlyCache, + cmId: options.cmId, + siteId: options.siteId, + }).then((result) => { for (let i = 0; i < result.entries.length; i++) { const entry = result.entries[i]; @@ -643,48 +653,34 @@ export class AddonModGlossaryProvider { * * @param fetchFunction Function to fetch. * @param fetchArguments Arguments to call the fetching. - * @param limitFrom Number of entries already fetched, so fetch will be done from this number. - * @param limitNum Number of records to return. Defaults to LIMIT_ENTRIES. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the response. */ - fetchEntries(fetchFunction: Function, fetchArguments: any[], limitFrom: number = 0, limitNum?: number, - omitExpires: boolean = false, forceOffline: boolean = false, siteId?: string): Promise { - limitNum = limitNum || AddonModGlossaryProvider.LIMIT_ENTRIES; - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - + fetchEntries(fetchFunction: Function, fetchArguments: any[], options: AddonModGlossaryGetEntriesOptions = {}): Promise { const args = fetchArguments.slice(); - args.push(limitFrom); - args.push(limitNum); - args.push(omitExpires); - args.push(forceOffline); - args.push(siteId); + args.push(options); return fetchFunction.apply(this, args); } /** - * Performs the whole fetch of the entries using the propper function and arguments. + * Performs the whole fetch of the entries using the proper function and arguments. * * @param fetchFunction Function to fetch. * @param fetchArguments Arguments to call the fetching. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with all entrries. */ - fetchAllEntries(fetchFunction: Function, fetchArguments: any[], omitExpires: boolean = false, forceOffline: boolean = false, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + fetchAllEntries(fetchFunction: Function, fetchArguments: any[], options: CoreCourseCommonModWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); const entries = []; - const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES; const fetchMoreEntries = (): Promise => { - return this.fetchEntries(fetchFunction, fetchArguments, entries.length, limitNum, omitExpires, forceOffline, siteId) - .then((result) => { + return this.fetchEntries(fetchFunction, fetchArguments, { + from: entries.length, + ...options, // Include all options. + }).then((result) => { Array.prototype.push.apply(entries, result.entries); return entries.length < result.count ? fetchMoreEntries() : entries; @@ -759,8 +755,11 @@ export class AddonModGlossaryProvider { const promises = []; if (!onlyEntriesList) { - promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true, false, siteId) - .then((entries) => { + promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], { + cmId: glossary.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((entries) => { return this.invalidateEntries(entries, siteId); })); } @@ -804,11 +803,11 @@ export class AddonModGlossaryProvider { * * @param courseId Course Id. * @param cmId Course Module Id. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the glossary. */ - getGlossary(courseId: number, cmId: number, siteId?: string): Promise { - return this.getCourseGlossaries(courseId, siteId).then((glossaries) => { + getGlossary(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getCourseGlossaries(courseId, options).then((glossaries) => { const glossary = glossaries.find((glossary) => glossary.coursemodule == cmId); if (glossary) { @@ -824,11 +823,11 @@ export class AddonModGlossaryProvider { * * @param courseId Course Id. * @param glossaryId Glossary Id. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the glossary. */ - getGlossaryById(courseId: number, glossaryId: number, siteId?: string): Promise { - return this.getCourseGlossaries(courseId, siteId).then((glossaries) => { + getGlossaryById(courseId: number, glossaryId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getCourseGlossaries(courseId, options).then((glossaries) => { const glossary = glossaries.find((glossary) => glossary.id == glossaryId); if (glossary) { @@ -846,28 +845,27 @@ export class AddonModGlossaryProvider { * @param concept Glossary entry concept. * @param definition Glossary entry concept definition. * @param courseId Course ID of the glossary. - * @param options Array of options for the entry. + * @param entryOptions Array of options for the entry. * @param attach Attachments ID if sending online, result of CoreFileUploaderProvider#storeFilesToUpload * otherwise. - * @param timeCreated The time the entry was created. If not defined, current time. - * @param siteId Site ID. If not defined, current site. - * @param discardEntry The entry provided will be discarded if found. - * @param allowOffline True if it can be stored in offline, false otherwise. - * @param checkDuplicates Check for duplicates before storing offline. Only used if allowOffline is true. + * @param otherOptions Other options. * @return Promise resolved with entry ID if entry was created in server, false if stored in device. */ - addEntry(glossaryId: number, concept: string, definition: string, courseId: number, options: any, attach: any, - timeCreated: number, siteId?: string, discardEntry?: any, allowOffline?: boolean, checkDuplicates?: boolean): - Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + addEntry(glossaryId: number, concept: string, definition: string, courseId: number, entryOptions: any, attach: any, + otherOptions: AddonModGlossaryAddEntryOptions = {}): Promise { + otherOptions.siteId = otherOptions.siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a new entry to be synchronized later. const storeOffline = (): Promise => { - const discardTime = discardEntry && discardEntry.timecreated; + const discardTime = otherOptions.discardEntry && otherOptions.discardEntry.timecreated; let duplicatesPromise; - if (checkDuplicates) { - duplicatesPromise = this.isConceptUsed(glossaryId, concept, discardTime, siteId); + if (otherOptions.checkDuplicates) { + duplicatesPromise = this.isConceptUsed(glossaryId, concept, { + cmId: otherOptions.cmId, + timeCreated: discardTime, + siteId: otherOptions.siteId, + }); } else { duplicatesPromise = Promise.resolve(false); } @@ -878,33 +876,34 @@ export class AddonModGlossaryProvider { return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists')); } - return this.glossaryOffline.addNewEntry(glossaryId, concept, definition, courseId, attach, options, timeCreated, - siteId, undefined, discardEntry).then(() => { + return this.glossaryOffline.addNewEntry(glossaryId, concept, definition, courseId, attach, entryOptions, + otherOptions.timeCreated, otherOptions.siteId, undefined, otherOptions.discardEntry).then(() => { return false; }); }); }; - if (!this.appProvider.isOnline() && allowOffline) { + if (!this.appProvider.isOnline() && otherOptions.allowOffline) { // App is offline, store the action. return storeOffline(); } // If we are editing an offline entry, discard previous first. let discardPromise; - if (discardEntry) { + if (otherOptions.discardEntry) { discardPromise = this.glossaryOffline.deleteNewEntry( - glossaryId, discardEntry.concept, discardEntry.timecreated, siteId); + glossaryId, otherOptions.discardEntry.concept, otherOptions.discardEntry.timecreated, otherOptions.siteId); } else { discardPromise = Promise.resolve(); } return discardPromise.then(() => { // Try to add it in online. - return this.addEntryOnline(glossaryId, concept, definition, options, attach, siteId).then((entryId) => { + return this.addEntryOnline(glossaryId, concept, definition, entryOptions, attach, otherOptions.siteId) + .then((entryId) => { return entryId; }).catch((error) => { - if (allowOffline && !this.utils.isWebServiceError(error)) { + if (otherOptions.allowOffline && !this.utils.isWebServiceError(error)) { // Couldn't connect to server, store in offline. return storeOffline(); } else { @@ -964,20 +963,23 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary ID. * @param concept Concept to check. - * @param timeCreated Timecreated to check that is not the timecreated we are editing. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with true if used, resolved with false if not used or error. */ - isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise { + isConceptUsed(glossaryId: number, concept: string, options: AddonModGlossaryIsConceptUsedOptions = {}): Promise { // Check offline first. - return this.glossaryOffline.isConceptUsed(glossaryId, concept, timeCreated, siteId).then((exists) => { + return this.glossaryOffline.isConceptUsed(glossaryId, concept, options.timeCreated, options.siteId).then((exists) => { if (exists) { return true; } // If we get here, there's no offline entry with this name, check online. // Get entries from the cache. - return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], true, false, siteId).then((entries) => { + return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((entries) => { // Check if there's any entry with the same concept. return entries.some((entry) => entry.concept == concept); }); @@ -1074,3 +1076,40 @@ export class AddonModGlossaryProvider { }); } } + +/** + * Options to pass to add entry. + */ +export type AddonModGlossaryAddEntryOptions = { + timeCreated?: number; // The time the entry was created. If not defined, current time. + discardEntry?: any; // The entry provided will be discarded if found. + allowOffline?: boolean; // True if it can be stored in offline, false otherwise. + checkDuplicates?: boolean; // Check for duplicates before storing offline. Only used if allowOffline is true. + cmId?: number; // Module ID. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to the different get entries functions. + */ +export type AddonModGlossaryGetEntriesOptions = CoreCourseCommonModWSOptions & { + from?: number; // Start returning records from here. Defaults to 0. + limit?: number; // Number of records to return. Defaults to AddonModGlossaryProvider.LIMIT_ENTRIES. +}; + +/** + * Options to pass to get categories. + */ +export type AddonModGlossaryGetCategoriesOptions = CoreCourseCommonModWSOptions & { + from?: number; // Start returning records from here. Defaults to 0. + limit?: number; // Number of records to return. Defaults to AddonModGlossaryProvider.LIMIT_CATEGORIES. +}; + +/** + * Options to pass to is concept used. + */ +export type AddonModGlossaryIsConceptUsedOptions = { + cmId?: number; // Module ID. + timeCreated?: number; // Timecreated to check that is not the timecreated we are editing. + siteId?: string; // Site ID. If not defined, current site. +}; diff --git a/src/addon/mod/glossary/providers/prefetch-handler.ts b/src/addon/mod/glossary/providers/prefetch-handler.ts index c202a88e6..cfb5c4785 100644 --- a/src/addon/mod/glossary/providers/prefetch-handler.ts +++ b/src/addon/mod/glossary/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -66,8 +66,9 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH */ getFiles(module: any, courseId: number, single?: boolean): Promise { return this.glossaryProvider.getGlossary(courseId, module.id).then((glossary) => { - return this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, [glossary.id, 'ALL']) - .then((entries) => { + return this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, [glossary.id, 'ALL'], { + cmId: module.id, + }).then((entries) => { return this.getFilesFromGlossaryAndEntries(module, glossary, entries); }); }).catch(() => { @@ -139,8 +140,14 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH protected prefetchGlossary(module: any, courseId: number, single: boolean, siteId: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + // Prefetch the glossary data. - return this.glossaryProvider.getGlossary(courseId, module.id, siteId).then((glossary) => { + return this.glossaryProvider.getGlossary(courseId, module.id, {siteId}).then((glossary) => { const promises = []; glossary.browsemodes.forEach((mode) => { @@ -149,25 +156,25 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH break; case 'cat': // Not implemented. promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByCategory, - [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES], false, false, siteId)); + [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES], options)); break; case 'date': promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate, - [glossary.id, 'CREATION', 'DESC'], false, false, siteId)); + [glossary.id, 'CREATION', 'DESC'], options)); promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate, - [glossary.id, 'UPDATE', 'DESC'], false, false, siteId)); + [glossary.id, 'UPDATE', 'DESC'], options)); break; case 'author': promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByAuthor, - [glossary.id, 'ALL', 'LASTNAME', 'ASC'], false, false, siteId)); + [glossary.id, 'ALL', 'LASTNAME', 'ASC'], options)); break; default: } }); // Fetch all entries to get information from. - promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, - [glossary.id, 'ALL'], false, false, siteId).then((entries) => { + promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, [glossary.id, 'ALL'], + options).then((entries) => { const promises = []; const commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(); @@ -190,7 +197,7 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH })); // Get all categories. - promises.push(this.glossaryProvider.getAllCategories(glossary.id, siteId)); + promises.push(this.glossaryProvider.getAllCategories(glossary.id, options)); // Prefetch data for link handlers. promises.push(this.courseProvider.getModuleBasicInfo(module.id, siteId)); diff --git a/src/addon/mod/glossary/providers/sync.ts b/src/addon/mod/glossary/providers/sync.ts index 5ad4034c5..87ea9ba5c 100644 --- a/src/addon/mod/glossary/providers/sync.ts +++ b/src/addon/mod/glossary/providers/sync.ts @@ -281,7 +281,7 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider { }); } if (result.warnings.length) { - promises.push(this.glossaryProvider.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, siteId) + promises.push(this.glossaryProvider.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, {siteId}) .then((glossary) => { result.warnings.forEach((warning) => { warnings.push(this.translate.instant('core.warningofflinedatadeleted', { diff --git a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html index 23df4f6ad..cd0c7f9e8 100644 --- a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html +++ b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html @@ -8,7 +8,7 @@ - + diff --git a/src/addon/mod/h5pactivity/components/index/index.ts b/src/addon/mod/h5pactivity/components/index/index.ts index 2dc01a2d5..c0f1077e7 100644 --- a/src/addon/mod/h5pactivity/components/index/index.ts +++ b/src/addon/mod/h5pactivity/components/index/index.ts @@ -108,7 +108,9 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv */ protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { try { - this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, false, this.siteId); + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, { + siteId: this.siteId, + }); this.dataRetrieved.emit(this.h5pActivity); this.description = this.h5pActivity.intro; @@ -161,7 +163,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv * @return Promise resolved when done. */ protected async fetchAccessInfo(): Promise { - this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, false, this.siteId); + this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, { + cmId: this.module.id, + siteId: this.siteId, + }); } /** diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts index 96b99fc16..996ddb0cf 100644 --- a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts @@ -77,32 +77,15 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit { * @return Promise resolved when done. */ protected async fetchData(): Promise { - await Promise.all([ - this.fetchActivity(), - this.fetchAttempt(), - ]); + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); + + this.attempt = await AddonModH5PActivity.instance.getAttemptResults(this.h5pActivityId, this.attemptId, { + cmId: this.h5pActivity.coursemodule, + }); await this.fetchUserProfile(); } - /** - * Get activity data. - * - * @return Promise resolved when done. - */ - protected async fetchActivity(): Promise { - this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); - } - - /** - * Get attempts. - * - * @return Promise resolved when done. - */ - protected async fetchAttempt(): Promise { - this.attempt = await AddonModH5PActivity.instance.getAttemptResults(this.h5pActivityId, this.attemptId); - } - /** * Get user profile. * diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts index cc8499405..bffee1f21 100644 --- a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts @@ -79,29 +79,24 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit { * @return Promise resolved when done. */ protected async fetchData(): Promise { + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); + await Promise.all([ - this.fetchActivity(), this.fetchAttempts(), this.fetchUserProfile(), ]); } - /** - * Get activity data. - * - * @return Promise resolved when done. - */ - protected async fetchActivity(): Promise { - this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); - } - /** * Get attempts. * * @return Promise resolved when done. */ protected async fetchAttempts(): Promise { - this.attemptsData = await AddonModH5PActivity.instance.getUserAttempts(this.h5pActivityId, { userId: this.userId }); + this.attemptsData = await AddonModH5PActivity.instance.getUserAttempts(this.h5pActivityId, { + cmId: this.h5pActivity.coursemodule, + userId: this.userId, + }); } /** diff --git a/src/addon/mod/h5pactivity/providers/h5pactivity.ts b/src/addon/mod/h5pactivity/providers/h5pactivity.ts index 5e1fabc42..1cf09d1c8 100644 --- a/src/addon/mod/h5pactivity/providers/h5pactivity.ts +++ b/src/addon/mod/h5pactivity/providers/h5pactivity.ts @@ -14,14 +14,15 @@ import { Injectable } from '@angular/core'; -import { CoreSites } from '@providers/sites'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; import { CoreTimeUtils } from '@providers/utils/time'; import { CoreUtils } from '@providers/utils/utils'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreCourseLogHelper } from '@core/course/providers/log-helper'; import { CoreH5P } from '@core/h5p/providers/h5p'; import { CoreH5PDisplayOptions } from '@core/h5p/classes/core'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { makeSingleton, Translate } from '@singletons/core.singletons'; @@ -121,20 +122,22 @@ export class AddonModH5PActivityProvider { * Get access information for a given H5P activity. * * @param id H5P activity ID. - * @param forceCache True to always get the value from cache. false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the data. */ - async getAccessInformation(id: number, forceCache?: boolean, siteId?: string): Promise { + async getAccessInformation(id: number, options: CoreCourseCommonModWSOptions = {}): Promise { - const site = await CoreSites.instance.getSite(siteId); + const site = await CoreSites.instance.getSite(options.siteId); const params = { h5pactivityid: id, }; const preSets = { cacheKey: this.getAccessInformationCacheKey(id), - omitExpires: forceCache, + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_h5pactivity_get_h5pactivity_access_information', params, preSets); @@ -209,18 +212,14 @@ export class AddonModH5PActivityProvider { h5pactivityid: id, attemptids: [attemptId], }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (options.forceCache) { - preSets.omitExpires = true; - } else if (options.ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - try { const response: AddonModH5PActivityGetResultsResult = await site.read('mod_h5pactivity_get_results', params, preSets); @@ -235,9 +234,12 @@ export class AddonModH5PActivityProvider { } // Check if the full list of results is cached. If so, get the results from there. - options.forceCache = true; + const cacheOptions = { + ...options, // Include all the original options. + readingStrategy: CoreSitesReadingStrategy.OnlyCache, + }; - const attemptsResults = await AddonModH5PActivity.instance.getAllAttemptsResults(id, options); + const attemptsResults = await AddonModH5PActivity.instance.getAllAttemptsResults(id, cacheOptions); const attempt = attemptsResults.attempts.find((attempt) => { return attempt.id == attemptId; @@ -270,18 +272,14 @@ export class AddonModH5PActivityProvider { h5pactivityid: id, attemptids: attemptsIds, }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getAttemptResultsCommonCacheKey(id), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (options.forceCache) { - preSets.omitExpires = true; - } else if (options.ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - const response: AddonModH5PActivityGetResultsResult = await site.read('mod_h5pactivity_get_results', params, preSets); response.attempts = response.attempts.map((attempt) => { @@ -334,28 +332,24 @@ export class AddonModH5PActivityProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param moduleUrl Module URL. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the activity data. */ - protected async getH5PActivityByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string) + protected async getH5PActivityByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) : Promise { - const site = await CoreSites.instance.getSite(siteId); + const site = await CoreSites.instance.getSite(options.siteId); const params = { courseids: [courseId], }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getH5PActivityDataCacheKey(courseId), updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModH5PActivityProvider.COMPONENT, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (forceCache) { - preSets.omitExpires = true; - } - const response: AddonModH5PActivityGetByCoursesResult = await site.read('mod_h5pactivity_get_h5pactivities_by_courses', params, preSets); @@ -377,12 +371,11 @@ export class AddonModH5PActivityProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the activity data. */ - getH5PActivity(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { - return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId); + getH5PActivity(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getH5PActivityByField(courseId, 'coursemodule', cmId, options); } /** @@ -390,13 +383,12 @@ export class AddonModH5PActivityProvider { * * @param courseId Course ID. * @param contextId Context ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the activity data. */ - getH5PActivityByContextId(courseId: number, contextId: number, forceCache?: boolean, siteId?: string) + getH5PActivityByContextId(courseId: number, contextId: number, options: CoreSitesCommonWSOptions = {}) : Promise { - return this.getH5PActivityByField(courseId, 'context', contextId, forceCache, siteId); + return this.getH5PActivityByField(courseId, 'context', contextId, options); } /** @@ -404,12 +396,11 @@ export class AddonModH5PActivityProvider { * * @param courseId Course ID. * @param id Instance ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the activity data. */ - getH5PActivityById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { - return this.getH5PActivityByField(courseId, 'id', id, forceCache, siteId); + getH5PActivityById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getH5PActivityByField(courseId, 'id', id, options); } /** @@ -440,9 +431,8 @@ export class AddonModH5PActivityProvider { * @param options Other options. * @return Promise resolved with the attempts of the user. */ - async getUserAttempts(id: number, options?: AddonModH5PActivityGetAttemptsOptions): Promise { - - options = options || {}; + async getUserAttempts(id: number, options: AddonModH5PActivityGetAttemptsOptions = {}) + : Promise { const site = await CoreSites.instance.getSite(options.siteId); @@ -450,18 +440,14 @@ export class AddonModH5PActivityProvider { h5pactivityid: id, userids: [options.userId || site.getUserId()], }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getUserAttemptsCacheKey(id, params.userids), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (options.forceCache) { - preSets.omitExpires = true; - } else if (options.ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - const response: AddonModH5PActivityGetAttemptsResult = await site.read('mod_h5pactivity_get_attempts', params, preSets); if (response.warnings[0]) { @@ -789,10 +775,7 @@ export type AddonModH5PActivityGetDeployedFileOptions = { /** * Options to pass to getAttemptResults function. */ -export type AddonModH5PActivityGetAttemptResultsOptions = { - forceCache?: boolean; // Whether to force cache. If not cached, it will call the WS. - ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down. - siteId?: string; // Site ID. If not defined, current site. +export type AddonModH5PActivityGetAttemptResultsOptions = CoreCourseCommonModWSOptions & { userId?: number; // User ID. If not defined, user of the site. }; diff --git a/src/addon/mod/h5pactivity/providers/prefetch-handler.ts b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts index 85a804baf..fd000532a 100644 --- a/src/addon/mod/h5pactivity/providers/prefetch-handler.ts +++ b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreWSExternalFile } from '@providers/ws'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -130,7 +130,10 @@ export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefet */ protected async prefetchActivity(module: any, courseId: number, single: boolean, siteId: string): Promise { - const h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(courseId, module.id, true, siteId); + const h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); const introFiles = this.getIntroFilesFromInstance(module, h5pActivity); @@ -171,14 +174,19 @@ export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefet */ protected async prefetchWSData(h5pActivity: AddonModH5PActivityData, siteId: string): Promise { - const accessInfo = await AddonModH5PActivity.instance.getAccessInformation(h5pActivity.id, true, siteId); + const accessInfo = await AddonModH5PActivity.instance.getAccessInformation(h5pActivity.id, { + cmId: h5pActivity.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }); if (!accessInfo.canreviewattempts) { // Not a teacher, prefetch user attempts and the current user profile. const site = await this.sitesProvider.getSite(siteId); const options = { - ignoreCache: true, + cmId: h5pActivity.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, siteId: siteId, }; diff --git a/src/addon/mod/h5pactivity/providers/sync.ts b/src/addon/mod/h5pactivity/providers/sync.ts index dafa44842..407f86d91 100644 --- a/src/addon/mod/h5pactivity/providers/sync.ts +++ b/src/addon/mod/h5pactivity/providers/sync.ts @@ -165,7 +165,7 @@ export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseP // Get the activity instance. const courseId = entries[0].courseid; - const h5pActivity = await AddonModH5PActivity.instance.getH5PActivityByContextId(courseId, contextId, false, siteId); + const h5pActivity = await AddonModH5PActivity.instance.getH5PActivityByContextId(courseId, contextId, {siteId}); // Sync offline logs. try { diff --git a/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html index 9f767d27c..7d14a8a56 100644 --- a/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html @@ -9,7 +9,7 @@ - + diff --git a/src/addon/mod/imscp/providers/imscp.ts b/src/addon/mod/imscp/providers/imscp.ts index bd7eb8cf4..3a0bd3c52 100644 --- a/src/addon/mod/imscp/providers/imscp.ts +++ b/src/addon/mod/imscp/providers/imscp.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -155,17 +155,21 @@ export class AddonModImscpProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the imscp is retrieved. */ - protected getImscpByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getImscpByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] + courseids: [courseId], }; const preSets = { cacheKey: this.getImscpDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModImscpProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_imscp_get_imscps_by_courses', params, preSets) @@ -188,11 +192,11 @@ export class AddonModImscpProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the imscp is retrieved. */ - getImscp(courseId: number, cmId: number, siteId?: string): Promise { - return this.getImscpByKey(courseId, 'coursemodule', cmId, siteId); + getImscp(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getImscpByKey(courseId, 'coursemodule', cmId, options); } /** diff --git a/src/addon/mod/imscp/providers/prefetch-handler.ts b/src/addon/mod/imscp/providers/prefetch-handler.ts index 6fe5ab2b9..899fdbe09 100644 --- a/src/addon/mod/imscp/providers/prefetch-handler.ts +++ b/src/addon/mod/imscp/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -67,7 +67,10 @@ export class AddonModImscpPrefetchHandler extends CoreCourseResourcePrefetchHand const promises = []; promises.push(super.downloadOrPrefetch(module, courseId, prefetch, dirPath)); - promises.push(this.imscpProvider.getImscp(courseId, module.id, siteId)); + promises.push(this.imscpProvider.getImscp(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + })); return Promise.all(promises); }); diff --git a/src/addon/mod/label/providers/label.ts b/src/addon/mod/label/providers/label.ts index ec924ebd0..b16a304fe 100644 --- a/src/addon/mod/label/providers/label.ts +++ b/src/addon/mod/label/providers/label.ts @@ -13,10 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** @@ -47,29 +47,22 @@ export class AddonModLabelProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache True to always get the value from cache, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not provided, current site. + * @param options Other options. * @return Promise resolved when the label is retrieved. */ - protected getLabelByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + protected getLabelByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getLabelDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getLabelDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModLabelProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_label_get_labels_by_courses', params, preSets) .then((response: AddonModLabelGetLabelsByCoursesResult): any => { @@ -91,14 +84,11 @@ export class AddonModLabelProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param forceCache True to always get the value from cache, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the label is retrieved. */ - getLabel(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { - return this.getLabelByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); + getLabel(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLabelByField(courseId, 'coursemodule', cmId, options); } /** @@ -106,14 +96,11 @@ export class AddonModLabelProvider { * * @param courseId Course ID. * @param labelId Label ID. - * @param forceCache True to always get the value from cache, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the label is retrieved. */ - getLabelById(courseId: number, labelId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { - return this.getLabelByField(courseId, 'id', labelId, forceCache, ignoreCache, siteId); + getLabelById(courseId: number, labelId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLabelByField(courseId, 'id', labelId, options); } /** diff --git a/src/addon/mod/label/providers/prefetch-handler.ts b/src/addon/mod/label/providers/prefetch-handler.ts index e05f9ac0c..b920ac7ba 100644 --- a/src/addon/mod/label/providers/prefetch-handler.ts +++ b/src/addon/mod/label/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -63,7 +63,9 @@ export class AddonModLabelPrefetchHandler extends CoreCourseResourcePrefetchHand let promise; if (this.labelProvider.isGetLabelAvailableForSite()) { - promise = this.labelProvider.getLabel(courseId, module.id, false, ignoreCache); + promise = this.labelProvider.getLabel(courseId, module.id, { + readingStrategy: ignoreCache ? CoreSitesReadingStrategy.OnlyNetwork : undefined + }); } else { promise = Promise.resolve(); } diff --git a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html index 834abcf38..ebe107144 100644 --- a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/lesson/components/index/index.ts b/src/addon/mod/lesson/components/index/index.ts index c8c4fb1ed..4df72774b 100644 --- a/src/addon/mod/lesson/components/index/index.ts +++ b/src/addon/mod/lesson/components/index/index.ts @@ -118,6 +118,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo let lessonReady = true; this.askPassword = false; + const options = {cmId: this.module.id}; return this.lessonProvider.getLesson(this.courseId, this.module.id).then((lessonData) => { this.lesson = lessonData; @@ -130,7 +131,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo return this.syncActivity(showErrors); } }).then(() => { - return this.lessonProvider.getAccessInformation(this.lesson.id); + return this.lessonProvider.getAccessInformation(this.lesson.id, options); }).then((info) => { const promises = []; @@ -167,8 +168,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo })); // Update the list of content pages viewed and question attempts. - promises.push(this.lessonProvider.getContentPagesViewedOnline(this.lesson.id, info.attemptscount)); - promises.push(this.lessonProvider.getQuestionsAttemptsOnline(this.lesson.id, info.attemptscount)); + promises.push(this.lessonProvider.getContentPagesViewedOnline(this.lesson.id, info.attemptscount, options)); + promises.push(this.lessonProvider.getQuestionsAttemptsOnline(this.lesson.id, info.attemptscount, options)); } if (info.preventaccessreasons && info.preventaccessreasons.length) { @@ -364,7 +365,9 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo if (this.hasOffline) { if (continueLast) { - promise = this.lessonProvider.getLastPageSeen(this.lesson.id, this.accessInfo.attemptscount); + promise = this.lessonProvider.getLastPageSeen(this.lesson.id, this.accessInfo.attemptscount, { + cmId: this.module.id, + }); } else { promise = Promise.resolve(this.accessInfo.firstpageid); } @@ -445,7 +448,10 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo } // Get the overview of retakes for the group. - return this.lessonProvider.getRetakesOverview(this.lesson.id, groupId).then((data) => { + return this.lessonProvider.getRetakesOverview(this.lesson.id, { + groupId, + cmId: this.lesson.coursemodule, + }).then((data) => { const promises = []; // Format times and grades. @@ -617,7 +623,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * @return Promise resolved when done. */ protected validatePassword(password: string): Promise { - return this.lessonProvider.getLessonWithPassword(this.lesson.id, password).then((lessonData) => { + return this.lessonProvider.getLessonWithPassword(this.lesson.id, {password, cmId: this.module.id}).then((lessonData) => { this.lesson = lessonData; this.password = password; }).catch((error) => { diff --git a/src/addon/mod/lesson/pages/player/player.ts b/src/addon/mod/lesson/pages/player/player.ts index 2b2c1753b..1f92465b9 100644 --- a/src/addon/mod/lesson/pages/player/player.ts +++ b/src/addon/mod/lesson/pages/player/player.ts @@ -18,7 +18,7 @@ import { IonicPage, NavParams, Content, PopoverController, ModalController, Moda import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -172,11 +172,10 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { * * @param func Function to call. * @param args Arguments to pass to the function. - * @param offlineParamPos Position of the offline parameter in the args. - * @param jumpsParamPos Position of the jumps parameter in the args. + * @param options Options passed to the function (also included in args). * @return Promise resolved in success, rejected otherwise. */ - protected callFunction(func: Function, args: any[], offlineParamPos: number, jumpsParamPos?: number): Promise { + protected callFunction(func: Function, args: any[], options: any): Promise { return func.apply(func, args).catch((error) => { if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson) && !this.utils.isWebServiceError(error)) { @@ -184,14 +183,16 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { this.offline = true; // Get the possible jumps now. - return this.lessonProvider.getPagesPossibleJumps(this.lesson.id, true).then((jumpList) => { + return this.lessonProvider.getPagesPossibleJumps(this.lesson.id, { + cmId: this.lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((jumpList) => { this.jumps = jumpList; - // Call the function again with offline set to true and the new jumps. - args[offlineParamPos] = true; - if (typeof jumpsParamPos != 'undefined') { - args[jumpsParamPos] = this.jumps; - } + // Call the function again with offline mode and the new jumps. + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + options.jumps = this.jumps; + options.offline = true; return func.apply(func, args); }); @@ -246,8 +247,13 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { this.offline = true; } + const options = { + cmId: this.lesson.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; + return this.callFunction(this.lessonProvider.getAccessInformation.bind(this.lessonProvider), - [this.lesson.id, this.offline, true], 1); + [this.lesson.id, options], options); }).then((info) => { const promises = []; @@ -272,15 +278,23 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { if (this.password) { // Lesson uses password, get the whole lesson object. + const options = { + password: this.password, + cmId: this.lesson.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; promises.push(this.callFunction(this.lessonProvider.getLessonWithPassword.bind(this.lessonProvider), - [this.lesson.id, this.password, true, this.offline, true], 3).then((lesson) => { + [this.lesson.id, options], options).then((lesson) => { this.lesson = lesson; })); } if (this.offline) { // Offline mode, get the list of possible jumps to allow navigation. - promises.push(this.lessonProvider.getPagesPossibleJumps(this.lesson.id, true).then((jumpList) => { + promises.push(this.lessonProvider.getPagesPossibleJumps(this.lesson.id, { + cmId: this.lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((jumpList) => { this.jumps = jumpList; })); } @@ -334,7 +348,9 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { const error = result.warnings[0]; // Some data was deleted. Check if the retake has changed. - return this.lessonProvider.getAccessInformation(this.lesson.id).then((info) => { + return this.lessonProvider.getAccessInformation(this.lesson.id, { + cmId: this.lesson.coursemodule, + }).then((info) => { if (info.attemptscount != this.accessInfo.attemptscount) { // The retake has changed. Leave the view and show the error. this.forceLeave = true; @@ -359,9 +375,16 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { return promise.then(() => { // Now finish the retake. - const args = [this.lesson, this.courseId, this.password, outOfTime, this.review, this.offline, this.accessInfo]; + const options = { + password: this.password, + outOfTime, + review: this.review, + offline: this.offline, + accessInfo: this.accessInfo, + }; + const args = [this.lesson, this.courseId, options]; - return this.callFunction(this.lessonProvider.finishRetake.bind(this.lessonProvider), args, 5); + return this.callFunction(this.lessonProvider.finishRetake.bind(this.lessonProvider), args, options); }).then((data) => { this.title = this.lesson.name; this.eolData = data.data; @@ -447,7 +470,10 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { if (this.lesson.timelimit && !this.accessInfo.canmanage) { // Get the last lesson timer. - return this.lessonProvider.getTimers(this.lesson.id, false, true).then((timers) => { + return this.lessonProvider.getTimers(this.lesson.id, { + cmId: this.lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }).then((timers) => { this.endTime = timers[timers.length - 1].starttime + this.lesson.timelimit; }); } @@ -469,9 +495,14 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { this.loadingMenu = true; - const args = [this.lessonId, this.password, this.offline, true]; + const options = { + password: this.password, + cmId: this.lesson.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; + const args = [this.lessonId, options]; - return this.callFunction(this.lessonProvider.getPages.bind(this.lessonProvider), args, 2).then((pages) => { + return this.callFunction(this.lessonProvider.getPages.bind(this.lessonProvider), args, options).then((pages) => { this.lessonPages = pages.map((entry) => { return entry.page; }); @@ -494,9 +525,19 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { return this.finishRetake(); } - const args = [this.lesson, pageId, this.password, this.review, true, this.offline, true, this.accessInfo, this.jumps]; + const options = { + password: this.password, + review: this.review, + includeContents: true, + cmId: this.lesson.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + accessInfo: this.accessInfo, + jumps: this.jumps, + includeOfflineData: true, + }; + const args = [this.lesson, pageId, options]; - return this.callFunction(this.lessonProvider.getPageData.bind(this.lessonProvider), args, 5, 8).then((data) => { + return this.callFunction(this.lessonProvider.getPageData.bind(this.lessonProvider), args, options).then((data) => { if (data.newpageid == AddonModLessonProvider.LESSON_EOL) { // End of lesson reached. return this.finishRetake(); @@ -548,10 +589,16 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { protected processPage(data: any, formSubmitted?: boolean): Promise { this.loaded = false; - const args = [this.lesson, this.courseId, this.pageData, data, this.password, this.review, this.offline, this.accessInfo, - this.jumps]; + const options = { + password: this.password, + review: this.review, + offline: this.offline, + accessInfo: this.accessInfo, + jumps: this.jumps, + }; + const args = [this.lesson, this.courseId, this.pageData, data, options]; - return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, 6, 8).then((result) => { + return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, options).then((result) => { if (formSubmitted) { this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.sitesProvider.getCurrentSiteId()); } @@ -559,11 +606,15 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson)) { // Lesson allows offline and the user changed some data in server. Update cached data. const retake = this.accessInfo.attemptscount; + const options = { + cmId: this.lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }; if (this.lessonProvider.isQuestionPage(this.pageData.page.type)) { - this.lessonProvider.getQuestionsAttemptsOnline(this.lessonId, retake, false, undefined, false, true); + this.lessonProvider.getQuestionsAttemptsOnline(this.lessonId, retake, options); } else { - this.lessonProvider.getContentPagesViewedOnline(this.lessonId, retake, false, true); + this.lessonProvider.getContentPagesViewedOnline(this.lessonId, retake, options); } } diff --git a/src/addon/mod/lesson/pages/user-retake/user-retake.ts b/src/addon/mod/lesson/pages/user-retake/user-retake.ts index bbbb9e6c8..3d6418495 100644 --- a/src/addon/mod/lesson/pages/user-retake/user-retake.ts +++ b/src/addon/mod/lesson/pages/user-retake/user-retake.ts @@ -106,7 +106,9 @@ export class AddonModLessonUserRetakePage implements OnInit { this.lesson = lessonData; // Get the retakes overview for all participants. - return this.lessonProvider.getRetakesOverview(this.lesson.id); + return this.lessonProvider.getRetakesOverview(this.lesson.id, { + cmId: this.lesson.coursemodule, + }); }).then((data) => { // Search the student. let student; @@ -193,7 +195,10 @@ export class AddonModLessonUserRetakePage implements OnInit { protected setRetake(retakeNumber: number): Promise { this.selectedRetake = retakeNumber; - return this.lessonProvider.getUserRetake(this.lessonId, retakeNumber, this.userId).then((data) => { + return this.lessonProvider.getUserRetake(this.lessonId, retakeNumber, { + cmId: this.lesson.coursemodule, + userId: this.userId, + }).then((data) => { if (data && data.completed != -1) { // Completed. diff --git a/src/addon/mod/lesson/providers/grade-link-handler.ts b/src/addon/mod/lesson/providers/grade-link-handler.ts index c608c692e..7c0cc6c34 100644 --- a/src/addon/mod/lesson/providers/grade-link-handler.ts +++ b/src/addon/mod/lesson/providers/grade-link-handler.ts @@ -57,7 +57,7 @@ export class AddonModLessonGradeLinkHandler extends CoreContentLinksModuleGradeH courseId = module.course || courseId || params.courseid || params.cid; // Check if the user can see the user reports in the lesson. - return this.lessonProvider.getAccessInformation(module.instance); + return this.lessonProvider.getAccessInformation(module.instance, {cmId: module.id, siteId}); }).then((info) => { if (info.canviewreports) { // User can view reports, go to view the report. diff --git a/src/addon/mod/lesson/providers/lesson-sync.ts b/src/addon/mod/lesson/providers/lesson-sync.ts index f3c2d4303..b7e4c86db 100644 --- a/src/addon/mod/lesson/providers/lesson-sync.ts +++ b/src/addon/mod/lesson/providers/lesson-sync.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -292,10 +292,14 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid courseId = attempts[0].courseid; // Get the info, access info and the lesson password if needed. - return this.lessonProvider.getLessonById(courseId, lessonId, false, false, siteId).then((lessonData) => { + return this.lessonProvider.getLessonById(courseId, lessonId, {siteId}).then((lessonData) => { lesson = lessonData; - return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); + return this.prefetchHandler.getLessonPassword(lessonId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + askPassword, + siteId, + }); }).then((data) => { const attemptsLength = attempts.length, promises = []; @@ -368,10 +372,14 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid // Data already retrieved when syncing attempts. promise = Promise.resolve(); } else { - promise = this.lessonProvider.getLessonById(courseId, lessonId, false, false, siteId).then((lessonData) => { + promise = this.lessonProvider.getLessonById(courseId, lessonId, {siteId}).then((lessonData) => { lesson = lessonData; - return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); + return this.prefetchHandler.getLessonPassword(lessonId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + askPassword, + siteId, + }); }).then((data) => { accessInfo = data.accessInfo; password = data.password; @@ -394,7 +402,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid } // All good, finish the retake. - return this.lessonProvider.finishRetakeOnline(lessonId, password, false, false, siteId).then((response) => { + return this.lessonProvider.finishRetakeOnline(lessonId, {password, siteId}).then((response) => { result.updated = true; if (!ignoreBlock) { @@ -403,7 +411,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid const params = this.urlUtils.extractUrlParams(response.data.reviewlesson.value); if (params && params.pageid) { // The retake can be reviewed, mark it as finished. Don't block the user for this. - this.setRetakeFinishedInSync(lessonId, retake.retake, params.pageid, siteId); + this.setRetakeFinishedInSync(lessonId, retake.retake, Number(params.pageid), siteId); } } } @@ -466,7 +474,10 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid protected sendAttempt(lesson: any, password: string, attempt: any, result: AddonModLessonSyncResult, siteId?: string) : Promise { - return this.lessonProvider.processPageOnline(lesson.id, attempt.pageid, attempt.data, password, false, siteId).then(() => { + return this.lessonProvider.processPageOnline(lesson.id, attempt.pageid, attempt.data, { + password, + siteId, + }).then(() => { result.updated = true; return this.lessonOfflineProvider.deleteAttempt(lesson.id, attempt.retake, attempt.pageid, attempt.timemodified, diff --git a/src/addon/mod/lesson/providers/lesson.ts b/src/addon/mod/lesson/providers/lesson.ts index a4db30c6f..380a01f7d 100644 --- a/src/addon/mod/lesson/providers/lesson.ts +++ b/src/addon/mod/lesson/providers/lesson.ts @@ -16,14 +16,15 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGradesProvider } from '@core/grades/providers/grades'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { AddonModLessonOfflineProvider } from './lesson-offline'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Result of check answer. @@ -314,32 +315,31 @@ export class AddonModLessonProvider { * Calculate some offline data like progress and ongoingscore. * * @param lesson Lesson. - * @param accessInfo Result of get access info. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param pageIndex Object containing all the pages indexed by ID. If not defined, it will be calculated. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the data. */ - protected calculateOfflineData(lesson: any, accessInfo?: any, password?: string, review?: boolean, pageIndex?: any, - siteId?: string): Promise<{reviewmode: boolean, progress: number, ongoingscore: string}> { + protected calculateOfflineData(lesson: any, options: AddonModLessonCalculateOfflineDataOptions = {}) + : Promise<{reviewmode: boolean, progress: number, ongoingscore: string}> { - accessInfo = accessInfo || {}; - - const reviewMode = review || accessInfo.reviewmode, + const accessInfo = options.accessInfo || {}; + const reviewMode = options.review || accessInfo.reviewmode, promises = []; let ongoingMessage = '', progress: number; if (!accessInfo.canmanage) { if (lesson.ongoing && !reviewMode) { - promises.push(this.getOngoingScoreMessage(lesson, accessInfo, password, review, pageIndex, siteId) - .then((message) => { + promises.push(this.getOngoingScoreMessage(lesson, accessInfo, options).then((message) => { ongoingMessage = message; })); } if (lesson.progressbar) { - promises.push(this.calculateProgress(lesson.id, accessInfo, password, review, pageIndex, siteId).then((p) => { + const modOptions = { + cmId: lesson.coursemodule, + ...options, // Include all options. + }; + + promises.push(this.calculateProgress(lesson.id, accessInfo, modOptions).then((p) => { progress = p; })); } @@ -366,37 +366,45 @@ export class AddonModLessonProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with a number: the progress (scale 0-100). */ - calculateProgress(lessonId: number, accessInfo: any, password?: string, review?: boolean, pageIndex?: any, siteId?: string) - : Promise { + calculateProgress(lessonId: number, accessInfo: any, options: AddonModLessonCalculateProgressOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); // Check if the user is reviewing the attempt. - if (review) { + if (options.review) { return Promise.resolve(100); } const retake = accessInfo.attemptscount; - let viewedPagesIds, - promise; + const commonOptions = { + cmId: options.cmId, + siteId: options.siteId, + }; + let viewedPagesIds; + let promise; - if (pageIndex) { + if (options.pageIndex) { promise = Promise.resolve(); } else { // Retrieve the index. - promise = this.getPages(lessonId, password, true, false, siteId).then((pages) => { - pageIndex = this.createPagesIndex(pages); + promise = this.getPages(lessonId, { + cmId: options.cmId, + password: options.password, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((pages) => { + options.pageIndex = this.createPagesIndex(pages); }); } return promise.then(() => { // Get the list of question pages attempted. - return this.getPagesIdsWithQuestionAttempts(lessonId, retake, false, siteId); + return this.getPagesIdsWithQuestionAttempts(lessonId, retake, commonOptions); }).then((ids) => { viewedPagesIds = ids; // Get the list of viewed content pages. - return this.getContentPagesViewedIds(lessonId, retake, siteId); + return this.getContentPagesViewedIds(lessonId, retake, commonOptions); }).then((viewedContentPagesIds) => { const validPages = {}; let pageId = accessInfo.firstpageid; @@ -410,7 +418,7 @@ export class AddonModLessonProvider { // Do not filter out Cluster Page(s) because we count a cluster as one. // By keeping the cluster page, we get our 1. while (pageId) { - pageId = this.validPageAndView(pageIndex, pageIndex[pageId], validPages, viewedPagesIds); + pageId = this.validPageAndView(options.pageIndex, options.pageIndex[pageId], validPages, viewedPagesIds); } // Progress calculation as a percent. @@ -998,23 +1006,24 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param courseId Course ID the lesson belongs to. - * @param password Lesson password (if any). - * @param outOfTime If the user ran out of time. - * @param review If the user wants to review just after finishing (1 hour margin). - * @param offline Whether it's offline mode. - * @param accessInfo Result of get access info. Required if offline is true. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved in success, rejected otherwise. */ - finishRetake(lesson: any, courseId: number, password?: string, outOfTime?: boolean, review?: boolean, offline?: boolean, - accessInfo?: any, siteId?: string): Promise { + finishRetake(lesson: any, courseId: number, options: AddonModLessonFinishRetakeOptions = {}): Promise { - if (offline) { - const retake = accessInfo.attemptscount; + if (options.offline) { + const retake = options.accessInfo.attemptscount; + const newOptions = { + cmId: lesson.coursemodule, + password: options.password, + review: options.review, + siteId: options.siteId, + }; - return this.lessonOfflineProvider.finishRetake(lesson.id, courseId, retake, true, outOfTime, siteId).then(() => { + return this.lessonOfflineProvider.finishRetake(lesson.id, courseId, retake, true, options.outOfTime, options.siteId) + .then(() => { // Get the lesson grade. - return this.lessonGrade(lesson, retake, password, review, undefined, siteId).catch(() => { + return this.lessonGrade(lesson, retake, newOptions).catch(() => { // Ignore errors. return {}; }); @@ -1034,7 +1043,7 @@ export class AddonModLessonProvider { this.addResultValueEolPage(result, 'offline', true); // Mark the result as offline. this.addResultValueEolPage(result, 'gradeinfo', gradeInfo); - if (lesson.custom && !accessInfo.canmanage) { + if (lesson.custom && !options.accessInfo.canmanage) { /* Before we calculate the custom score make sure they answered the minimum number of questions. We only need to do this for custom scoring as we can not get the miniumum score the user should achieve. If we are not using custom scoring (so all questions are valued as 1) then we simply check if they @@ -1052,10 +1061,9 @@ export class AddonModLessonProvider { } } - if (!accessInfo.canmanage) { + if (!options.accessInfo.canmanage) { if (gradeLesson) { - promises.push(this.calculateProgress(lesson.id, accessInfo, password, review, undefined, siteId) - .then((progress) => { + promises.push(this.calculateProgress(lesson.id, options.accessInfo, newOptions).then((progress) => { this.addResultValueEolPage(result, 'progresscompleted', progress); })); @@ -1094,7 +1102,7 @@ export class AddonModLessonProvider { } else { // User hasn't answered any question, only content pages. if (lesson.timelimit) { - if (outOfTime) { + if (options.outOfTime) { this.addResultValueEolPage(result, 'eolstudentoutoftimenoanswers', true, true); } } else { @@ -1109,7 +1117,7 @@ export class AddonModLessonProvider { } } - if (lesson.modattempts && accessInfo.canmanage) { + if (lesson.modattempts && options.accessInfo.canmanage) { this.addResultValueEolPage(result, 'modattemptsnoteacher', true, true); } @@ -1121,13 +1129,13 @@ export class AddonModLessonProvider { }); } - return this.finishRetakeOnline(lesson.id, password, outOfTime, review, siteId).then((response) => { + return this.finishRetakeOnline(lesson.id, options).then((response) => { this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { lessonId: lesson.id, type: 'finish', courseId: courseId, - outOfTime: outOfTime, - review: review + outOfTime: options.outOfTime, + review: options.review, }, this.sitesProvider.getCurrentSiteId()); return response; @@ -1138,23 +1146,20 @@ export class AddonModLessonProvider { * Finishes a retake. It will fail if offline or cannot connect. * * @param lessonId Lesson ID. - * @param password Lesson password (if any). - * @param outOfTime If the user ran out of time. - * @param review If the user wants to review just after finishing (1 hour margin). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved in success, rejected otherwise. */ - finishRetakeOnline(lessonId: number, password?: string, outOfTime?: boolean, review?: boolean, siteId?: string): Promise { + finishRetakeOnline(lessonId: number, options: AddonModLessonFinishRetakeOnlineOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { lessonid: lessonId, - outoftime: outOfTime ? 1 : 0, - review: review ? 1 : 0 + outoftime: options.outOfTime ? 1 : 0, + review: options.review ? 1 : 0, }; - if (typeof password == 'string') { - params.password = password; + if (typeof options.password == 'string') { + params.password = options.password; } return site.write('mod_lesson_finish_attempt', params).then((response) => { @@ -1180,26 +1185,21 @@ export class AddonModLessonProvider { * Get the access information of a certain lesson. * * @param lessonId Lesson ID. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the access information. */ - getAccessInformation(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAccessInformation(lessonId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - lessonid: lessonId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAccessInformationCacheKey(lessonId) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + }; + const preSets = { + cacheKey: this.getAccessInformationCacheKey(lessonId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_lesson_access_information', params, preSets); }); @@ -1220,10 +1220,11 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object with the online and offline viewed pages. */ - getContentPagesViewed(lessonId: number, retake: number, siteId?: string): Promise<{online: any[], offline: any[]}> { + getContentPagesViewed(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}) + : Promise<{online: any[], offline: any[]}> { const promises = [], type = AddonModLessonProvider.TYPE_STRUCTURE, result = { @@ -1232,12 +1233,12 @@ export class AddonModLessonProvider { }; // Get the online pages. - promises.push(this.getContentPagesViewedOnline(lessonId, retake, false, false, siteId).then((pages) => { + promises.push(this.getContentPagesViewedOnline(lessonId, retake, options).then((pages) => { result.online = pages; })); // Get the offline pages. - promises.push(this.lessonOfflineProvider.getRetakeAttemptsForType(lessonId, retake, type, siteId).catch(() => { + promises.push(this.lessonOfflineProvider.getRetakeAttemptsForType(lessonId, retake, type, options.siteId).catch(() => { return []; }).then((pages) => { result.offline = pages; @@ -1274,11 +1275,11 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with list of IDs. */ - getContentPagesViewedIds(lessonId: number, retake: number, siteId?: string): Promise { - return this.getContentPagesViewed(lessonId, retake, siteId).then((result) => { + getContentPagesViewedIds(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.getContentPagesViewed(lessonId, retake, options).then((result) => { const ids = {}, pages = result.online.concat(result.offline); @@ -1299,29 +1300,22 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the viewed pages. */ - getContentPagesViewedOnline(lessonId: number, retake: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getContentPagesViewedOnline(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - lessonid: lessonId, - lessonattempt: retake - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getContentPagesViewedCacheKey(lessonId, retake) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + lessonattempt: retake, + }; + const preSets = { + cacheKey: this.getContentPagesViewedCacheKey(lessonId, retake), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_content_pages_viewed', params, preSets).then((result) => { return result.pages; @@ -1334,11 +1328,11 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the last content page viewed. */ - getLastContentPageViewed(lessonId: number, retake: number, siteId?: string): Promise { - return this.getContentPagesViewed(lessonId, retake, siteId).then((data) => { + getLastContentPageViewed(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.getContentPagesViewed(lessonId, retake, options).then((data) => { let lastPage, maxTime = 0; @@ -1368,22 +1362,22 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the last page seen. */ - getLastPageSeen(lessonId: number, retake: number, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getLastPageSeen(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); let lastPageSeen: number; // Get the last question answered. - return this.lessonOfflineProvider.getLastQuestionPageAttempt(lessonId, retake, siteId).then((answer) => { + return this.lessonOfflineProvider.getLastQuestionPageAttempt(lessonId, retake, options.siteId).then((answer) => { if (answer) { lastPageSeen = answer.newpageid; } // Now get the last content page viewed. - return this.getLastContentPageViewed(lessonId, retake, siteId).then((page) => { + return this.getLastContentPageViewed(lessonId, retake, options).then((page) => { if (page) { if (answer) { if (page.timemodified > answer.timemodified) { @@ -1406,13 +1400,11 @@ export class AddonModLessonProvider { * * @param courseId Course ID. * @param cmid Course module ID. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the lesson is retrieved. */ - getLesson(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.getLessonByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); + getLesson(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLessonByField(courseId, 'coursemodule', cmId, options); } /** @@ -1421,29 +1413,21 @@ export class AddonModLessonProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the lesson is retrieved. */ - protected getLessonByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + protected getLessonByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getLessonDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getLessonDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModLessonProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_lessons_by_courses', params, preSets).then((response) => { if (response && response.lessons) { @@ -1466,13 +1450,11 @@ export class AddonModLessonProvider { * * @param courseId Course ID. * @param id Lesson ID. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the lesson is retrieved. */ - getLessonById(courseId: number, id: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.getLessonByField(courseId, 'id', id, forceCache, ignoreCache, siteId); + getLessonById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLessonByField(courseId, 'id', id, options); } /** @@ -1489,34 +1471,25 @@ export class AddonModLessonProvider { * Get a lesson protected with password. * * @param lessonId Lesson ID. - * @param password Password. - * @param validatePassword If true, the function will fail if the password is wrong. - * If false, it will return a lesson with the basic data if password is wrong. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the lesson. */ - getLessonWithPassword(lessonId: number, password?: string, validatePassword: boolean = true, forceCache?: boolean, - ignoreCache?: boolean, siteId?: string): Promise { + getLessonWithPassword(lessonId: number, options: AddonModLessonGetWithPasswordOptions = {}): Promise { + const validatePassword = typeof options.validatePassword == 'undefined' ? true : options.validatePassword; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - lessonid: lessonId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getLessonWithPasswordCacheKey(lessonId) - }; + lessonid: lessonId, + }; + const preSets = { + cacheKey: this.getLessonWithPasswordCacheKey(lessonId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof password == 'string') { - params.password = password; - } - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; + if (typeof options.password == 'string') { + params.password = options.password; } return site.read('mod_lesson_get_lesson', params, preSets).then((response) => { @@ -1574,24 +1547,20 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param accessInfo Result of get access info. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param pageIndex Object containing all the pages indexed by ID. If not provided, it will be calculated. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the ongoing score message. */ - getOngoingScoreMessage(lesson: any, accessInfo: any, password?: string, review?: boolean, pageIndex?: any, siteId?: string) - : Promise { + getOngoingScoreMessage(lesson: any, accessInfo: any, options: AddonModLessonGradeOptions = {}): Promise { if (accessInfo.canmanage) { return Promise.resolve(this.translate.instant('addon.mod_lesson.teacherongoingwarning')); } else { let retake = accessInfo.attemptscount; - if (review) { + if (options.review) { retake--; } - return this.lessonGrade(lesson, retake, password, review, pageIndex, siteId).then((gradeInfo) => { + return this.lessonGrade(lesson, retake, options).then((gradeInfo) => { const data: any = {}; if (lesson.custom) { @@ -1614,13 +1583,16 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param pageId Page ID. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of possible answers. */ - protected getPageAnswers(lesson: any, pageId: number, password?: string, review?: boolean, siteId?: string): Promise { - return this.getPageData(lesson, pageId, password, review, true, true, false, undefined, undefined, siteId).then((data) => { + protected getPageAnswers(lesson: any, pageId: number, options: AddonModLessonPwdReviewOptions = {}): Promise { + return this.getPageData(lesson, pageId, { + includeContents: true, + ...options, // Include all options. + readingStrategy: options.readingStrategy || CoreSitesReadingStrategy.PreferCache, + includeOfflineData: false, + }).then((data) => { return data.answers; }); } @@ -1630,19 +1602,16 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param pageIds List of page IDs. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object containing the answers. */ - protected getPagesAnswers(lesson: any, pageIds: number[], password?: string, review?: boolean, siteId?: string) - : Promise { + protected getPagesAnswers(lesson: any, pageIds: number[], options: AddonModLessonPwdReviewOptions = {}): Promise { const answers = {}, promises = []; pageIds.forEach((pageId) => { - promises.push(this.getPageAnswers(lesson, pageId, password, review, siteId).then((pageAnswers) => { + promises.push(this.getPageAnswers(lesson, pageId, options).then((pageAnswers) => { pageAnswers.forEach((answer) => { // Include the pageid in each answer and add them to the final list. answer.pageid = pageId; @@ -1661,42 +1630,30 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param pageId Page ID. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param includeContents Include the page rendered contents. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param accessInfo Result of get access info. Required if offline is true. - * @param jumps Result of get pages possible jumps. Required if offline is true. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the page data. */ - getPageData(lesson: any, pageId: number, password?: string, review?: boolean, includeContents?: boolean, forceCache?: boolean, - ignoreCache?: boolean, accessInfo?: any, jumps?: any, siteId?: string): Promise { + getPageData(lesson: any, pageId: number, options: AddonModLessonGetPageDataOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - lessonid: lesson.id, - pageid: Number(pageId), - review: review ? 1 : 0, - returncontents: includeContents ? 1 : 0 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getPageDataCacheKey(lesson.id, pageId) - }; + lessonid: lesson.id, + pageid: Number(pageId), + review: options.review ? 1 : 0, + returncontents: options.includeContents ? 1 : 0, + }; + const preSets = { + cacheKey: this.getPageDataCacheKey(lesson.id, pageId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof password == 'string') { - params.password = password; + if (typeof options.password == 'string') { + params.password = options.password; } - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - - if (review) { + if (options.review) { // Force online mode in review. preSets.getFromCache = false; preSets.saveToCache = false; @@ -1704,12 +1661,15 @@ export class AddonModLessonProvider { } return site.read('mod_lesson_get_page_data', params, preSets).then((data) => { - if (forceCache && accessInfo && data.page) { + if (preSets.omitExpires && options.includeOfflineData && data.page) { // Offline mode and valid page. Calculate the data that might be affected. - return this.calculateOfflineData(lesson, accessInfo, password, review, undefined, siteId).then((calcData) => { + return this.calculateOfflineData(lesson, options).then((calcData) => { Object.assign(data, calcData); - return this.getPageViewMessages(lesson, accessInfo, data.page, review, jumps, password, siteId); + return this.getPageViewMessages(lesson, options.accessInfo, data.page, options.jumps, { + password: options.password, + siteId: options.siteId, + }); }).then((messages) => { data.messages = messages; @@ -1747,32 +1707,25 @@ export class AddonModLessonProvider { * Get lesson pages. * * @param lessonId Lesson ID. - * @param password Lesson password (if any). - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the pages. */ - getPages(lessonId: number, password?: string, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + getPages(lessonId: number, options: AddonModLessonPwdReviewOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - lessonid: lessonId, - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getPagesCacheKey(lessonId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; + lessonid: lessonId, + }; + const preSets = { + cacheKey: this.getPagesCacheKey(lessonId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof password == 'string') { - params.password = password; - } - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; + if (typeof options.password == 'string') { + params.password = options.password; } return site.read('mod_lesson_get_pages', params, preSets).then((response) => { @@ -1795,27 +1748,21 @@ export class AddonModLessonProvider { * Get possible jumps for a lesson. * * @param lessonId Lesson ID. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the jumps. */ - getPagesPossibleJumps(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + getPagesPossibleJumps(lessonId: number, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - lessonid: lessonId, - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getPagesPossibleJumpsCacheKey(lessonId) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + }; + const preSets = { + cacheKey: this.getPagesPossibleJumpsCacheKey(lessonId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_pages_possible_jumps', params, preSets).then((response) => { // Index the jumps by page and jumpto. @@ -1889,15 +1836,13 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param correct True to only fetch correct attempts, false to get them all. - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's user. + * @param options Other options. * @return Promise resolved with the IDs. */ - getPagesIdsWithQuestionAttempts(lessonId: number, retake: number, correct?: boolean, siteId?: string, userId?: number) + getPagesIdsWithQuestionAttempts(lessonId: number, retake: number, options: AddonModLessonGetPagesIdsWithAttemptsOptions = {}) : Promise { - return this.getQuestionsAttempts(lessonId, retake, correct, undefined, siteId, userId).then((result) => { + return this.getQuestionsAttempts(lessonId, retake, options).then((result) => { const ids = {}, attempts = result.online.concat(result.offline); @@ -1921,13 +1866,11 @@ export class AddonModLessonProvider { * @param lesson Lesson. * @param accessInfo Result of get access info. Required if offline is true. * @param page Page loaded. - * @param review If the user wants to review just after finishing (1 hour margin). * @param jumps Result of get pages possible jumps. - * @param password Lesson password (if any). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of messages. */ - getPageViewMessages(lesson: any, accessInfo: any, page: any, review: boolean, jumps: any, password?: string, siteId?: string) + getPageViewMessages(lesson: any, accessInfo: any, page: any, jumps: any, options: AddonModLessonGetPageViewMessagesOptions = {}) : Promise { const messages = []; @@ -1938,7 +1881,7 @@ export class AddonModLessonProvider { // Tell student how many questions they have seen, how many are required and their grade. const retake = accessInfo.attemptscount; - promise = this.lessonGrade(lesson, retake, password, review, undefined, siteId).then((gradeInfo) => { + promise = this.lessonGrade(lesson, retake, options).then((gradeInfo) => { if (gradeInfo.attempts) { if (gradeInfo.nquestions < lesson.minquestions) { this.addMessage(messages, 'addon.mod_lesson.numberofpagesviewednotice', {$a: { @@ -1947,7 +1890,7 @@ export class AddonModLessonProvider { }}); } - if (!review && !lesson.retake) { + if (!options.review && !lesson.retake) { this.addMessage(messages, 'addon.mod_lesson.numberofcorrectanswers', {$a: gradeInfo.earned}); if (lesson.grade != CoreGradesProvider.TYPE_NONE) { @@ -1986,13 +1929,10 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param correct True to only fetch correct attempts, false to get them all. - * @param pageId If defined, only get attempts on this page. - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's user. + * @param options Other options. * @return Promise resolved with the questions attempts. */ - getQuestionsAttempts(lessonId: number, retake: number, correct?: boolean, pageId?: number, siteId?: string, userId?: number) + getQuestionsAttempts(lessonId: number, retake: number, options: AddonModLessonGetQuestionsAttemptsOptions = {}) : Promise<{online: any[], offline: any[]}> { const promises = [], @@ -2001,12 +1941,12 @@ export class AddonModLessonProvider { offline: [] }; - promises.push(this.getQuestionsAttemptsOnline(lessonId, retake, correct, pageId, false, false, siteId, userId) - .then((attempts) => { + promises.push(this.getQuestionsAttemptsOnline(lessonId, retake, options).then((attempts) => { result.online = attempts; })); - promises.push(this.lessonOfflineProvider.getQuestionsAttempts(lessonId, retake, correct, pageId, siteId).catch(() => { + promises.push(this.lessonOfflineProvider.getQuestionsAttempts(lessonId, retake, options.correct, options.pageId, + options.siteId).catch(() => { // Error, assume no attempts. return []; }).then((attempts) => { @@ -2045,46 +1985,37 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param correct True to only fetch correct attempts, false to get them all. - * @param pageId If defined, only get attempts on this page. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's user. + * @param options Other options. * @return Promise resolved with the questions attempts. */ - getQuestionsAttemptsOnline(lessonId: number, retake: number, correct?: boolean, pageId?: number, forceCache?: boolean, - ignoreCache?: boolean, siteId?: string, userId?: number): Promise { + getQuestionsAttemptsOnline(lessonId: number, retake: number, options: AddonModLessonGetQuestionsAttemptsOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); // Don't pass "pageId" and "correct" params, they will be filtered locally. const params = { - lessonid: lessonId, - attempt: retake, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuestionsAttemptsCacheKey(lessonId, retake, userId) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + attempt: retake, + userid: userId, + }; + const preSets = { + cacheKey: this.getQuestionsAttemptsCacheKey(lessonId, retake, userId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_questions_attempts', params, preSets).then((response) => { - if (pageId || correct) { + if (options.pageId || options.correct) { // Filter the attempts. return response.attempts.filter((attempt) => { - if (correct && !attempt.correct) { + if (options.correct && !attempt.correct) { return false; } - if (pageId && attempt.pageid != pageId) { + if (options.pageId && attempt.pageid != options.pageId) { return false; } @@ -2101,33 +2032,25 @@ export class AddonModLessonProvider { * Get the overview of retakes in a lesson (named "attempts overview" in Moodle). * * @param lessonId Lesson ID. - * @param groupId The group to get. If not defined, all participants. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the retakes overview. */ - getRetakesOverview(lessonId: number, groupId?: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getRetakesOverview(lessonId: number, options: AddonModLessonGroupOptions = {}): Promise { - groupId = groupId || 0; + const groupId = options.groupId || 0; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - lessonid: lessonId, - groupid: groupId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getRetakesOverviewCacheKey(lessonId, groupId), - updateFrequency: CoreSite.FREQUENCY_OFTEN - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + groupid: groupId, + }; + const preSets = { + cacheKey: this.getRetakesOverviewCacheKey(lessonId, groupId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_attempts_overview', params, preSets).then((response) => { return response.data; @@ -2204,30 +2127,23 @@ export class AddonModLessonProvider { * Get lesson timers. * * @param lessonId Lesson ID. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's current user. + * @param options Other options. * @return Promise resolved with the pages. */ - getTimers(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string, userId?: number): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + getTimers(lessonId: number, options: AddonModLessonUserOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - lessonid: lessonId, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getTimersCacheKey(lessonId, userId) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + userid: userId, + }; + const preSets = { + cacheKey: this.getTimersCacheKey(lessonId, userId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_user_timers', params, preSets).then((response) => { return response.timers; @@ -2331,34 +2247,26 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number - * @param userId User ID. Undefined for current user. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the retake data. */ - getUserRetake(lessonId: number, retake: number, userId?: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getUserRetake(lessonId: number, retake: number, options: AddonModLessonUserOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - lessonid: lessonId, - userid: userId, - lessonattempt: retake - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getUserRetakeCacheKey(lessonId, userId, retake), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + userid: userId, + lessonattempt: retake, + }; + const preSets = { + cacheKey: this.getUserRetakeCacheKey(lessonId, userId, retake), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_user_attempt', params, preSets); }); @@ -2870,15 +2778,10 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param retake Retake number. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param pageIndex Object containing all the pages indexed by ID. If not provided, it will be calculated. - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's user. + * @param options Other options. * @return Promise resolved with the grade data. */ - lessonGrade(lesson: any, retake: number, password?: string, review?: boolean, pageIndex?: any, siteId?: string, - userId?: number): Promise { + lessonGrade(lesson: any, retake: number, options: AddonModLessonGradeOptions = {}): Promise { // Initialize all variables. let nViewed = 0, @@ -2890,7 +2793,11 @@ export class AddonModLessonProvider { earned = 0; // Get the questions attempts for the user. - return this.getQuestionsAttempts(lesson.id, retake, false, undefined, siteId, userId).then((attemptsData) => { + return this.getQuestionsAttempts(lesson.id, retake, { + cmId: lesson.coursemodule, + siteId: options.siteId, + userId: options.userId, + }).then((attemptsData) => { const attempts = attemptsData.online.concat(attemptsData.offline); if (!attempts.length) { @@ -2902,9 +2809,14 @@ export class AddonModLessonProvider { let promise; // Create the pageIndex if it isn't provided. - if (!pageIndex) { - promise = this.getPages(lesson.id, password, true, false, siteId).then((pages) => { - pageIndex = this.createPagesIndex(pages); + if (!options.pageIndex) { + promise = this.getPages(lesson.id, { + password: options.password, + cmId: lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((pages) => { + options.pageIndex = this.createPagesIndex(pages); }); } else { promise = Promise.resolve(); @@ -2922,18 +2834,20 @@ export class AddonModLessonProvider { attemptSet[attempt.pageid].push(attempt); }); - // Drop all attempts that go beyond max attempts for the lesson. - for (const pageId in attemptSet) { - // Sort the list by time in ascending order. - const attempts = attemptSet[pageId].sort((a, b) => { - return (a.timeseen || a.timemodified) - (b.timeseen || b.timemodified); - }); + if (lesson.maxattempts > 0) { + // Drop all attempts that go beyond max attempts for the lesson. + for (const pageId in attemptSet) { + // Sort the list by time in ascending order. + const attempts = attemptSet[pageId].sort((a, b) => { + return (a.timeseen || a.timemodified) - (b.timeseen || b.timemodified); + }); - attemptSet[pageId] = attempts.slice(0, lesson.maxattempts); + attemptSet[pageId] = attempts.slice(0, lesson.maxattempts); + } } // Get all the answers from the pages the user answered. - return this.getPagesAnswers(lesson, pageIds, password, review, siteId); + return this.getPagesAnswers(lesson, pageIds, options); }).then((answers) => { // Number of pages answered. nQuestions = Object.keys(attemptSet).length; @@ -2944,7 +2858,7 @@ export class AddonModLessonProvider { if (lesson.custom) { // If essay question, handle it, otherwise add to score. - if (pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) { + if (options.pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) { if (lastAttempt.useranswer && typeof lastAttempt.useranswer.score != 'undefined') { earned += lastAttempt.useranswer.score; } @@ -2959,7 +2873,7 @@ export class AddonModLessonProvider { }); // If essay question, increase numbers. - if (pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) { + if (options.pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) { nManual++; manualPoints++; } @@ -3046,31 +2960,32 @@ export class AddonModLessonProvider { * @param courseId Course ID the lesson belongs to. * @param pageData Result of getPageData for the page to process. * @param data Data to save. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param offline Whether it's offline mode. - * @param accessInfo Result of get access info. Required if offline is true. - * @param jumps Result of get pages possible jumps. Required if offline is true. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - processPage(lesson: any, courseId: number, pageData: any, data: any, password?: string, review?: boolean, offline?: boolean, - accessInfo?: boolean, jumps?: any, siteId?: string): Promise { + processPage(lesson: any, courseId: number, pageData: any, data: any, options: AddonModLessonProcessPageOptions = {}) + : Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); const page = pageData.page, pageId = page.id; let result, pageIndex; - if (offline) { + if (options.offline) { // Get the list of pages of the lesson. - return this.getPages(lesson.id, password, true, false, siteId).then((pages) => { + return this.getPages(lesson.id, { + cmId: lesson.coursemodule, + password: options.password, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((pages) => { pageIndex = this.createPagesIndex(pages); if (pageData.answers.length) { - return this.recordAttempt(lesson, courseId, pageData, data, review, accessInfo, jumps, pageIndex, siteId); + return this.recordAttempt(lesson, courseId, pageData, data, options.review, options.accessInfo, options.jumps, + pageIndex, options.siteId); } else { // The page has no answers so we will just progress to the next page (as set by newpageid). return { @@ -3080,15 +2995,21 @@ export class AddonModLessonProvider { } }).then((res) => { result = res; - result.newpageid = this.getNewPageId(pageData.page.id, result.newpageid, jumps); + result.newpageid = this.getNewPageId(pageData.page.id, result.newpageid, options.jumps); // Calculate some needed offline data. - return this.calculateOfflineData(lesson, accessInfo, password, review, pageIndex, siteId); + return this.calculateOfflineData(lesson, { + accessInfo: options.accessInfo, + password: options.password, + review: options.review, + pageIndex, + siteId: options.siteId, + }); }).then((calculatedData) => { // Add some default data to match the WS response. result.warnings = []; result.displaymenu = pageData.displaymenu; // Keep the same value since we can't calculate it in offline. - result.messages = this.getPageProcessMessages(lesson, accessInfo, result, review, jumps); + result.messages = this.getPageProcessMessages(lesson, options.accessInfo, result, options.review, options.jumps); result.sent = false; Object.assign(result, calculatedData); @@ -3096,13 +3017,13 @@ export class AddonModLessonProvider { }); } - return this.processPageOnline(lesson.id, pageId, data, password, review, siteId).then((response) => { + return this.processPageOnline(lesson.id, pageId, data, options).then((response) => { this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { lessonId: lesson.id, type: 'process', courseId: courseId, pageId: pageId, - review: review + review: options.review, }, this.sitesProvider.getCurrentSiteId()); response.sent = true; @@ -3117,24 +3038,22 @@ export class AddonModLessonProvider { * @param lessonId Lesson ID. * @param pageId Page ID. * @param data Data to save. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved in success, rejected otherwise. */ - processPageOnline(lessonId: number, pageId: number, data: any, password?: string, review?: boolean, siteId?: string) + processPageOnline(lessonId: number, pageId: number, data: any, options: AddonModLessonProcessPageOnlineOptions = {}) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { lessonid: lessonId, pageid: pageId, data: this.utils.objectToArrayOfObjects(data, 'name', 'value', true), - review: review ? 1 : 0 + review: options.review ? 1 : 0, }; - if (typeof password == 'string') { - params.password = password; + if (typeof options.password == 'string') { + params.password = options.password; } return site.write('mod_lesson_process_page', params); @@ -3189,11 +3108,15 @@ export class AddonModLessonProvider { } else { if (!accessInfo.canmanage) { // Get the number of attempts that have been made on this question for this student and retake. - promise = this.getQuestionsAttempts(lesson.id, retake, false, pageData.page.id, siteId).then((attempts) => { + promise = this.getQuestionsAttempts(lesson.id, retake, { + cmId: lesson.coursemodule, + pageId: pageData.page.id, + siteId, + }).then((attempts) => { nAttempts = attempts.online.length + attempts.offline.length; // Check if they have reached (or exceeded) the maximum number of attempts allowed. - if (nAttempts >= lesson.maxattempts) { + if (lesson.maxattempts > 0 && nAttempts >= lesson.maxattempts) { result.maxattemptsreached = true; result.feedback = this.translate.instant('addon.mod_lesson.maximumnumberofattemptsreached'); result.newpageid = AddonModLessonProvider.LESSON_NEXTPAGE; @@ -3219,12 +3142,12 @@ export class AddonModLessonProvider { // Check if "number of attempts remaining" message is needed. if (!result.correctanswer && !result.newpageid) { // Retreive the number of attempts left counter. - if (nAttempts >= lesson.maxattempts) { + if (lesson.maxattempts > 0 && nAttempts >= lesson.maxattempts) { if (lesson.maxattempts > 1) { // Don't bother with message if only one attempt. result.maxattemptsreached = true; } result.newpageid = AddonModLessonProvider.LESSON_NEXTPAGE; - } else if (lesson.maxattempts > 1) { // Don't bother with message if only one attempt + } else if (lesson.maxattempts > 1) { // Don't bother with message if only one attempt or unlimited. result.attemptsremaining = lesson.maxattempts - nAttempts; } } @@ -3264,8 +3187,11 @@ export class AddonModLessonProvider { if (lesson.review && !result.correctanswer && !result.isessayquestion) { // Calculate the number of question attempt in the page if it isn't calculated already. if (typeof nAttempts == 'undefined') { - subPromise = this.getQuestionsAttempts(lesson.id, retake, false, pageData.page.id, siteId) - .then((result) => { + subPromise = this.getQuestionsAttempts(lesson.id, retake, { + cmId: lesson.coursemodule, + pageId: pageData.page.id, + siteId, + }).then((result) => { nAttempts = result.online.length + result.offline.length; }); } else { @@ -3399,3 +3325,142 @@ export class AddonModLessonProvider { return page.nextpageid; } } + +/** + * Common options including a group ID. + */ +export type AddonModLessonGroupOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // The group to get. If not defined, all participants. +}; + +/** + * Common options including a group ID. + */ +export type AddonModLessonUserOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User ID. If not defined, site's current user. +}; + +/** + * Common options including a password. + */ +export type AddonModLessonPasswordOptions = CoreCourseCommonModWSOptions & { + password?: string; // Lesson password (if any). +}; + +/** + * Common options including password and review. + */ +export type AddonModLessonPwdReviewOptions = AddonModLessonPasswordOptions & { + review?: boolean; // If the user wants to review just after finishing (1 hour margin). +}; + +/** + * Options to pass to get lesson with password. + */ +export type AddonModLessonGetWithPasswordOptions = AddonModLessonPasswordOptions & { + validatePassword?: boolean; // Defauls to true. If true, the function will fail if the password is wrong. + // If false, it will return a lesson with the basic data if password is wrong. +}; + +/** + * Options to pass to calculateProgress. + */ +export type AddonModLessonCalculateProgressBasicOptions = { + password?: string; // Lesson password (if any). + review?: boolean; // If the user wants to review just after finishing (1 hour margin). + pageIndex?: any; // Object containing all the pages indexed by ID. If not provided, it will be calculated. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to calculateProgress. + */ +export type AddonModLessonCalculateProgressOptions = AddonModLessonCalculateProgressBasicOptions & { + cmId?: number; // Module ID. +}; + +/** + * Options to pass to lessonGrade. + */ +export type AddonModLessonGradeOptions = AddonModLessonCalculateProgressBasicOptions & { + userId?: number; // User ID. If not defined, site's user. +}; + +/** + * Options to pass to calculateOfflineData. + */ +export type AddonModLessonCalculateOfflineDataOptions = AddonModLessonCalculateProgressBasicOptions & { + accessInfo?: any; // Result of get access info. +}; + +/** + * Options to pass to get page data. + */ +export type AddonModLessonGetPageDataOptions = AddonModLessonPwdReviewOptions & { + includeContents?: boolean; // Include the page rendered contents. + includeOfflineData?: boolean; // Whether to include calculated offline data. Only when ignoring cache. + accessInfo?: any; // Result of get access info. Required if offline is true. + jumps?: any; // Result of get pages possible jumps. Required if offline is true. +}; + +/** + * Options to pass to get page data. + */ +export type AddonModLessonGetPageViewMessagesOptions = { + password?: string; // Lesson password (if any). + review?: boolean; // If the user wants to review just after finishing (1 hour margin). + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to get questions attempts. + */ +export type AddonModLessonGetQuestionsAttemptsOptions = CoreCourseCommonModWSOptions & { + correct?: boolean; // True to only fetch correct attempts, false to get them all. + pageId?: number; // If defined, only get attempts on this page. + userId?: number; // User ID. If not defined, site's user. +}; + +/** + * Options to pass to getPagesIdsWithQuestionAttempts. + */ +export type AddonModLessonGetPagesIdsWithAttemptsOptions = CoreCourseCommonModWSOptions & { + correct?: boolean; // True to only fetch correct attempts, false to get them all. + userId?: number; // User ID. If not defined, site's user. +}; + +/** + * Options to pass to processPageOnline. + */ +export type AddonModLessonProcessPageOnlineOptions = { + password?: string; // Lesson password (if any). + review?: boolean; // If the user wants to review just after finishing (1 hour margin). + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to processPage. + */ +export type AddonModLessonProcessPageOptions = AddonModLessonProcessPageOnlineOptions & { + offline?: boolean; // Whether it's offline mode. + accessInfo?: any; // Result of get access info. Required if offline is true. + jumps?: any; // Result of get pages possible jumps. Required if offline is true. +}; + +/** + * Options to pass to finishRetakeOnline. + */ +export type AddonModLessonFinishRetakeOnlineOptions = { + password?: string; // Lesson password (if any). + outOfTime?: boolean; // Whether the user ran out of time. + review?: boolean; // If the user wants to review just after finishing (1 hour margin). + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to finishRetake. + */ +export type AddonModLessonFinishRetakeOptions = AddonModLessonFinishRetakeOnlineOptions & { + offline?: boolean; // Whether it's offline mode. + accessInfo?: any; // Result of get access info. Required if offline is true. +}; diff --git a/src/addon/mod/lesson/providers/prefetch-handler.ts b/src/addon/mod/lesson/providers/prefetch-handler.ts index c8601ae58..4610f4de3 100644 --- a/src/addon/mod/lesson/providers/prefetch-handler.ts +++ b/src/addon/mod/lesson/providers/prefetch-handler.ts @@ -17,10 +17,10 @@ import { ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { AddonModLessonProvider } from './lesson'; @@ -98,11 +98,15 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan password, result; - return this.lessonProvider.getLesson(courseId, module.id, false, false, siteId).then((lessonData) => { + return this.lessonProvider.getLesson(courseId, module.id, {siteId}).then((lessonData) => { lesson = lessonData; // Get the lesson password if it's needed. - return this.getLessonPassword(lesson.id, false, true, single, siteId); + return this.getLessonPassword(lesson.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + askPassword: single, + siteId, + }); }).then((data) => { password = data.password; lesson = data.lesson || lesson; @@ -116,7 +120,11 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan result = res; // Get the pages to calculate the size. - return this.lessonProvider.getPages(lesson.id, password, false, false, siteId); + return this.lessonProvider.getPages(lesson.id, { + cmId: module.id, + password, + siteId, + }); }).then((pages) => { pages.forEach((page) => { result.size += page.filessizetotal; @@ -130,19 +138,16 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan * Get the lesson password if needed. If not stored, it can ask the user to enter it. * * @param lessonId Lesson ID. - * @param forceCache Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param askPassword True if we should ask for password if needed, false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - getLessonPassword(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, askPassword?: boolean, siteId?: string) + getLessonPassword(lessonId: number, options: AddonModLessonGetPasswordOptions = {}) : Promise<{password?: string, lesson?: any, accessInfo: any}> { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); // Get access information to check if password is needed. - return this.lessonProvider.getAccessInformation(lessonId, forceCache, ignoreCache, siteId).then((info): any => { + return this.lessonProvider.getAccessInformation(lessonId, options).then((info): any => { if (info.preventaccessreasons && info.preventaccessreasons.length) { const passwordNeeded = info.preventaccessreasons.length == 1 && this.lessonProvider.isPasswordProtected(info); if (passwordNeeded) { @@ -152,15 +157,15 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan // No password found. }).then((password) => { if (password) { - return this.validatePassword(lessonId, info, password, forceCache, ignoreCache, siteId); + return this.validatePassword(lessonId, info, password, options); } else { return Promise.reject(null); } }).catch(() => { // No password or error validating it. Ask for it if allowed. - if (askPassword) { + if (options.askPassword) { return this.askUserPassword(info).then((password) => { - return this.validatePassword(lessonId, info, password, forceCache, ignoreCache, siteId); + return this.validatePassword(lessonId, info, password, options); }); } @@ -207,7 +212,10 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan const siteId = this.sitesProvider.getCurrentSiteId(); // Invalidate data to determine if module is downloadable. - return this.lessonProvider.getLesson(courseId, module.id, true, false, siteId).then((lesson) => { + return this.lessonProvider.getLesson(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((lesson) => { const promises = []; promises.push(this.lessonProvider.invalidateLessonData(courseId, siteId)); @@ -227,9 +235,9 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan isDownloadable(module: any, courseId: number): boolean | Promise { const siteId = this.sitesProvider.getCurrentSiteId(); - return this.lessonProvider.getLesson(courseId, module.id, false, false, siteId).then((lesson) => { + return this.lessonProvider.getLesson(courseId, module.id, {siteId}).then((lesson) => { // Check if there is any prevent access reason. - return this.lessonProvider.getAccessInformation(lesson.id, false, false, siteId).then((info) => { + return this.lessonProvider.getAccessInformation(lesson.id, {cmId: module.id, siteId}).then((info) => { if (!info.canviewreports && !this.lessonProvider.isLessonOffline(lesson)) { return false; } @@ -273,15 +281,28 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when done. */ protected prefetchLesson(module: any, courseId: number, single: boolean, siteId: string): Promise { - let lesson, - password, - accessInfo; + let lesson; + let password; + let accessInfo; - return this.lessonProvider.getLesson(courseId, module.id, false, true, siteId).then((lessonData) => { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + + return this.lessonProvider.getLesson(courseId, module.id, commonOptions).then((lessonData) => { lesson = lessonData; // Get the lesson password if it's needed. - return this.getLessonPassword(lesson.id, false, true, single, siteId); + return this.getLessonPassword(lesson.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + askPassword: single, + siteId, + }); }).then((data) => { password = data.password; lesson = data.lesson || lesson; @@ -297,7 +318,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan // Ignore errors. })); - promises.push(this.lessonProvider.getAccessInformation(lesson.id, false, true, siteId).then((info) => { + promises.push(this.lessonProvider.getAccessInformation(lesson.id, modOptions).then((info) => { accessInfo = info; })); @@ -316,7 +337,12 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan // Get the list of pages. if (this.lessonProvider.isLessonOffline(lesson)) { - promises.push(this.lessonProvider.getPages(lesson.id, password, false, true, siteId).then((pages) => { + const passwordOptions = { + password, + ...modOptions, // Include all mod options. + }; + + promises.push(this.lessonProvider.getPages(lesson.id, passwordOptions).then((pages) => { const subPromises = []; let hasRandomBranch = false; @@ -333,8 +359,11 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan } // Get the page data. We don't pass accessInfo because we don't need to calculate the offline data. - subPromises.push(this.lessonProvider.getPageData(lesson, data.page.id, password, false, true, false, - true, undefined, undefined, siteId).then((pageData) => { + subPromises.push(this.lessonProvider.getPageData(lesson, data.page.id, { + includeContents: true, + includeOfflineData: false, + ...passwordOptions, // Include all options. + }).then((pageData) => { // Download the page files. let pageFiles = pageData.contentfiles || []; @@ -353,7 +382,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan }); // Prefetch the list of possible jumps for offline navigation. Do it here because we know hasRandomBranch. - subPromises.push(this.lessonProvider.getPagesPossibleJumps(lesson.id, false, true, siteId).catch((error) => { + subPromises.push(this.lessonProvider.getPagesPossibleJumps(lesson.id, modOptions).catch((error) => { if (hasRandomBranch) { // The WebSevice probably failed because RANDOMBRANCH aren't supported if the user hasn't seen any page. return Promise.reject(this.translate.instant('addon.mod_lesson.errorprefetchrandombranch')); @@ -366,16 +395,15 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan })); // Prefetch user timers to be able to calculate timemodified in offline. - promises.push(this.lessonProvider.getTimers(lesson.id, false, true, siteId).catch(() => { + promises.push(this.lessonProvider.getTimers(lesson.id, modOptions).catch(() => { // Ignore errors. })); // Prefetch viewed pages in last retake to calculate progress. - promises.push(this.lessonProvider.getContentPagesViewedOnline(lesson.id, retake, false, true, siteId)); + promises.push(this.lessonProvider.getContentPagesViewedOnline(lesson.id, retake, modOptions)); // Prefetch question attempts in last retake for offline calculations. - promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lesson.id, retake, false, undefined, false, true, - siteId)); + promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lesson.id, retake, modOptions)); } if (accessInfo.canviewreports) { @@ -384,11 +412,14 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan const subPromises = []; info.groups.forEach((group) => { - subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, group.id, false, true, siteId)); + subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, { + groupId: group.id, + ...modOptions, // Include all options. + })); }); - // Always get group 0, even if there are no groups. - subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, 0, false, true, siteId).then((data) => { + // Always get all participants, even if there are no groups. + subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, modOptions).then((data) => { if (!data || !data.students) { return; } @@ -406,8 +437,10 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan return; } - retakePromises.push(this.lessonProvider.getUserRetake(lesson.id, lastRetake.try, student.id, false, - true, siteId).then((attempt) => { + retakePromises.push(this.lessonProvider.getUserRetake(lesson.id, lastRetake.try, { + userId: student.id, + ...modOptions, // Include all options. + }).then((attempt) => { if (!attempt || !attempt.answerpages) { return; } @@ -445,19 +478,20 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan * @param lessonId Lesson ID. * @param info Lesson access info. * @param pwd Password to check. - * @param forceCache Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - protected validatePassword(lessonId: number, info: any, pwd: string, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise<{password: string, lesson: any, accessInfo: any}> { + protected validatePassword(lessonId: number, info: any, pwd: string, options: CoreCourseCommonModWSOptions = {}) + : Promise<{password: string, lesson: any, accessInfo: any}> { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.lessonProvider.getLessonWithPassword(lessonId, pwd, true, forceCache, ignoreCache, siteId).then((lesson) => { + return this.lessonProvider.getLessonWithPassword(lessonId, { + password: pwd, + ...options, // Include all options. + }).then((lesson) => { // Password is ok, store it and return the data. - return this.lessonProvider.storePassword(lesson.id, pwd, siteId).then(() => { + return this.lessonProvider.storePassword(lesson.id, pwd, options.siteId).then(() => { return { password: pwd, lesson: lesson, @@ -483,3 +517,10 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan return this.syncProvider.syncLesson(module.instance, false, false, siteId); } } + +/** + * Options to pass to get lesson password. + */ +export type AddonModLessonGetPasswordOptions = CoreCourseCommonModWSOptions & { + askPassword?: boolean; // True if we should ask for password if needed, false otherwise. +}; diff --git a/src/addon/mod/lti/providers/lti.ts b/src/addon/mod/lti/providers/lti.ts index acae1b745..b2d301078 100644 --- a/src/addon/mod/lti/providers/lti.ts +++ b/src/addon/mod/lti/providers/lti.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; @@ -100,29 +100,32 @@ export class AddonModLtiProvider { * * @param courseId Course ID. * @param cmId Course module ID. + * @param options Other options. * @return Promise resolved when the LTI is retrieved. */ - getLti(courseId: number, cmId: number): Promise { + async getLti(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { const params: any = { courseids: [courseId] }; - const preSets: any = { + const preSets = { cacheKey: this.getLtiCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModLtiProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - return this.sitesProvider.getCurrentSite().read('mod_lti_get_ltis_by_courses', params, preSets) - .then((response: AddonModLtiGetLtisByCoursesResult): any => { + const site = await this.sitesProvider.getSite(options.siteId); - if (response.ltis) { - const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId); - if (currentLti) { - return currentLti; - } + const response: AddonModLtiGetLtisByCoursesResult = await site.read('mod_lti_get_ltis_by_courses', params, preSets); + + if (response.ltis) { + const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId); + if (currentLti) { + return currentLti; } + } - return Promise.reject(null); - }); + throw new Error('Activity not found.'); } /** diff --git a/src/addon/mod/page/components/index/addon-mod-page-index.html b/src/addon/mod/page/components/index/addon-mod-page-index.html index d3d6c7726..783080214 100644 --- a/src/addon/mod/page/components/index/addon-mod-page-index.html +++ b/src/addon/mod/page/components/index/addon-mod-page-index.html @@ -6,7 +6,7 @@ - + diff --git a/src/addon/mod/page/providers/page.ts b/src/addon/mod/page/providers/page.ts index 817e848b3..2fab4bcce 100644 --- a/src/addon/mod/page/providers/page.ts +++ b/src/addon/mod/page/providers/page.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -43,11 +43,11 @@ export class AddonModPageProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the page is retrieved. */ - getPageData(courseId: number, cmId: number, siteId?: string): Promise { - return this.getPageByKey(courseId, 'coursemodule', cmId, siteId); + getPageData(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getPageByKey(courseId, 'coursemodule', cmId, options); } /** @@ -56,18 +56,21 @@ export class AddonModPageProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the page is retrieved. */ - protected getPageByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getPageByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getPageCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getPageCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModPageProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_page_get_pages_by_courses', params, preSets) .then((response: AddonModPageGetPagesByCoursesResult): any => { diff --git a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html index ec8685448..d7586c3ac 100644 --- a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html +++ b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/quiz/components/index/index.ts b/src/addon/mod/quiz/components/index/index.ts index 3a2157138..d970c5e88 100644 --- a/src/addon/mod/quiz/components/index/index.ts +++ b/src/addon/mod/quiz/components/index/index.ts @@ -202,7 +202,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp } // Get quiz access info. - return this.quizProvider.getQuizAccessInformation(this.quizData.id).then((info) => { + return this.quizProvider.getQuizAccessInformation(this.quizData.id, {cmId: this.module.id}).then((info) => { this.quizAccessInfo = info; this.quizData.showReviewColumn = info.canreviewmyattempts; this.accessRules = info.accessrules; @@ -213,7 +213,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp } // Get question types in the quiz. - return this.quizProvider.getQuizRequiredQtypes(this.quizData.id).then((types) => { + return this.quizProvider.getQuizRequiredQtypes(this.quizData.id, {cmId: this.module.id}).then((types) => { this.unsupportedQuestions = this.quizProvider.getUnsupportedQuestions(types); this.hasSupportedQuestions = !!types.find((type) => { return type != 'random' && this.unsupportedQuestions.indexOf(type) == -1; @@ -239,11 +239,11 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp protected getAttempts(): Promise { // Get access information of last attempt (it also works if no attempts made). - return this.quizProvider.getAttemptAccessInformation(this.quizData.id, 0).then((info) => { + return this.quizProvider.getAttemptAccessInformation(this.quizData.id, 0, {cmId: this.module.id}).then((info) => { this.attemptAccessInfo = info; // Get attempts. - return this.quizProvider.getUserAttempts(this.quizData.id).then((atts) => { + return this.quizProvider.getUserAttempts(this.quizData.id, {cmId: this.module.id}).then((atts) => { return this.treatAttempts(atts).then((atts) => { this.attempts = atts; @@ -355,7 +355,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp if (this.quizData.showFeedbackColumn) { // Get the quiz overall feedback. - return this.quizProvider.getFeedbackForGrade(this.quizData.id, this.gradebookData.grade).then((response) => { + return this.quizProvider.getFeedbackForGrade(this.quizData.id, this.gradebookData.grade, { + cmId: this.module.id, + }).then((response) => { this.overallFeedback = response.feedbacktext; }); } @@ -379,7 +381,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp const attemptId = this.autoReview.attemptId; if (this.quizAccessInfo.canreviewmyattempts) { - return this.quizProvider.getAttemptReview(attemptId, -1).then(() => { + return this.quizProvider.getAttemptReview(attemptId, {page: -1, cmId: this.module.id}).then(() => { this.navCtrl.push('AddonModQuizReviewPage', {courseId: this.courseId, quizId: this.quizData.id, attemptId}); }).catch(() => { // Ignore errors. @@ -559,12 +561,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp promises.push(this.quizProvider.loadFinishedOfflineData(attempts)); // Get combined review options. - promises.push(this.quizProvider.getCombinedReviewOptions(this.quizData.id).then((result) => { + promises.push(this.quizProvider.getCombinedReviewOptions(this.quizData.id, {cmId: this.module.id}).then((result) => { this.options = result; })); // Get best grade. - promises.push(this.quizProvider.getUserBestGrade(this.quizData.id).then((best) => { + promises.push(this.quizProvider.getUserBestGrade(this.quizData.id, {cmId: this.module.id}).then((best) => { this.bestGrade = best; // Get gradebook grade. diff --git a/src/addon/mod/quiz/pages/attempt/attempt.ts b/src/addon/mod/quiz/pages/attempt/attempt.ts index dae185cc3..d4e3d1909 100644 --- a/src/addon/mod/quiz/pages/attempt/attempt.ts +++ b/src/addon/mod/quiz/pages/attempt/attempt.ts @@ -92,7 +92,7 @@ export class AddonModQuizAttemptPage implements OnInit { accessInfo; // Get all the attempts and search the one we want. - promises.push(this.quizProvider.getUserAttempts(this.quizId).then((attempts) => { + promises.push(this.quizProvider.getUserAttempts(this.quizId, {cmId: this.quiz.coursemodule}).then((attempts) => { for (let i = 0; i < attempts.length; i++) { const attempt = attempts[i]; if (attempt.id == this.attemptId) { @@ -110,12 +110,13 @@ export class AddonModQuizAttemptPage implements OnInit { return this.quizProvider.loadFinishedOfflineData([this.attempt]); })); - promises.push(this.quizProvider.getCombinedReviewOptions(this.quiz.id).then((opts) => { + promises.push(this.quizProvider.getCombinedReviewOptions(this.quiz.id, {cmId: this.quiz.coursemodule}).then((opts) => { options = opts; })); // Check if the user can review the attempt. - promises.push(this.quizProvider.getQuizAccessInformation(this.quiz.id).then((quizAccessInfo) => { + promises.push(this.quizProvider.getQuizAccessInformation(this.quiz.id, {cmId: this.quiz.coursemodule}) + .then((quizAccessInfo) => { accessInfo = quizAccessInfo; if (accessInfo.canreviewmyattempts) { @@ -123,7 +124,7 @@ export class AddonModQuizAttemptPage implements OnInit { return this.quizProvider.invalidateAttemptReviewForPage(this.attemptId, -1).catch(() => { // Ignore errors. }).then(() => { - return this.quizProvider.getAttemptReview(this.attemptId, -1); + return this.quizProvider.getAttemptReview(this.attemptId, {page: -1, cmId: this.quiz.coursemodule}); }).catch(() => { // Error getting the review, assume the user cannot review the attempt. accessInfo.canreviewmyattempts = false; @@ -146,7 +147,9 @@ export class AddonModQuizAttemptPage implements OnInit { options.someoptions.overallfeedback && !isNaN(grade)) { // Feedback should be displayed, get the feedback for the grade. - return this.quizProvider.getFeedbackForGrade(this.quiz.id, grade).then((response) => { + return this.quizProvider.getFeedbackForGrade(this.quiz.id, grade, { + cmId: this.quiz.coursemodule, + }).then((response) => { this.attempt.feedback = response.feedbacktext; }); } else { diff --git a/src/addon/mod/quiz/pages/player/player.html b/src/addon/mod/quiz/pages/player/player.html index 9936714cf..4dfb5fdce 100644 --- a/src/addon/mod/quiz/pages/player/player.html +++ b/src/addon/mod/quiz/pages/player/player.html @@ -58,7 +58,7 @@ - +
diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts index 653a9b89a..fe33b00d9 100644 --- a/src/addon/mod/quiz/pages/player/player.ts +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -17,7 +17,7 @@ import { IonicPage, NavParams, Content, PopoverController, ModalController, Moda import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -315,12 +315,18 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { } // Get access information for the quiz. - return this.quizProvider.getQuizAccessInformation(this.quiz.id, this.offline, true); + return this.quizProvider.getQuizAccessInformation(this.quiz.id, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }); }).then((info) => { this.quizAccessInfo = info; // Get user attempts to determine last attempt. - return this.quizProvider.getUserAttempts(this.quiz.id, 'all', true, this.offline, true); + return this.quizProvider.getUserAttempts(this.quiz.id, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }); }).then((attempts) => { if (!attempts.length) { // There are no attempts, start a new one. @@ -396,8 +402,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { */ protected fixSequenceChecks(): Promise { // Get current page data again to get the latest sequencechecks. - return this.quizProvider.getAttemptData(this.attempt.id, this.attempt.currentpage, this.preflightData, this.offline, true) - .then((data) => { + return this.quizProvider.getAttemptData(this.attempt.id, this.attempt.currentpage, this.preflightData, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((data) => { const newSequenceChecks = {}; @@ -443,7 +451,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ protected loadPage(page: number): Promise { - return this.quizProvider.getAttemptData(this.attempt.id, page, this.preflightData, this.offline, true).then((data) => { + return this.quizProvider.getAttemptData(this.attempt.id, page, this.preflightData, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((data) => { // Update attempt, status could change during the execution. this.attempt = data.attempt; this.attempt.currentpage = page; @@ -487,7 +498,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { protected loadSummary(): Promise { this.summaryQuestions = []; - return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline, true, true).then((qs) => { + return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, { + cmId: this.quiz.coursemodule, + loadLocal: this.offline, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((qs) => { this.showSummary = true; this.summaryQuestions = qs; @@ -511,8 +526,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { */ protected loadNavigation(): Promise { // We use the attempt summary to build the navigation because it contains all the questions. - return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline, true, true) - .then((questions) => { + return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, { + cmId: this.quiz.coursemodule, + loadLocal: this.offline, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((questions) => { questions.forEach((question) => { question.stateClass = this.questionHelper.getQuestionStateClass(question.state); @@ -551,7 +569,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { // Prepare the answers to be sent for the attempt. protected prepareAnswers(): Promise { - return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline); + return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline, this.component, + this.quiz.coursemodule); } /** @@ -564,7 +583,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { */ protected processAttempt(userFinish?: boolean, timeUp?: boolean, retrying?: boolean): Promise { // Get the answers to send. - return this.prepareAnswers().then((answers) => { + let promise = Promise.resolve({}); + + if (!this.showSummary) { + promise = this.prepareAnswers(); + } + + return promise.then((answers) => { // Send the answers. return this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData, userFinish, timeUp, this.offline).catch((error) => { @@ -594,6 +619,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { if (this.formElement) { this.domUtils.triggerFormSubmittedEvent(this.formElement, !this.offline, this.sitesProvider.getCurrentSiteId()); } + + return this.questionHelper.clearTmpData(this.questions, this.component, this.quiz.coursemodule); }); } @@ -652,7 +679,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { false, 'addon.mod_quiz.startattempt').then((attempt) => { // Re-fetch attempt access information with the right attempt (might have changed because a new attempt was created). - return this.quizProvider.getAttemptAccessInformation(this.quiz.id, attempt.id, this.offline, true).then((info) => { + return this.quizProvider.getAttemptAccessInformation(this.quiz.id, attempt.id, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((info) => { this.attemptAccessInfo = info; this.attempt = attempt; diff --git a/src/addon/mod/quiz/pages/review/review.html b/src/addon/mod/quiz/pages/review/review.html index e61d8388b..612020eb1 100644 --- a/src/addon/mod/quiz/pages/review/review.html +++ b/src/addon/mod/quiz/pages/review/review.html @@ -77,7 +77,7 @@ - +
diff --git a/src/addon/mod/quiz/pages/review/review.ts b/src/addon/mod/quiz/pages/review/review.ts index 5c2ef225d..50dfd53b8 100644 --- a/src/addon/mod/quiz/pages/review/review.ts +++ b/src/addon/mod/quiz/pages/review/review.ts @@ -134,7 +134,7 @@ export class AddonModQuizReviewPage implements OnInit { this.quiz = quizData; this.componentId = this.quiz.coursemodule; - return this.quizProvider.getCombinedReviewOptions(this.quizId).then((result) => { + return this.quizProvider.getCombinedReviewOptions(this.quizId, {cmId: this.quiz.coursemodule}).then((result) => { this.options = result; // Load the navigation data. @@ -155,7 +155,7 @@ export class AddonModQuizReviewPage implements OnInit { * @return Promise resolved when done. */ protected loadPage(page: number): Promise { - return this.quizProvider.getAttemptReview(this.attemptId, page).then((data) => { + return this.quizProvider.getAttemptReview(this.attemptId, {page, cmId: this.quiz.coursemodule}).then((data) => { this.attempt = data.attempt; this.attempt.currentpage = page; this.currentPage = page; @@ -187,7 +187,7 @@ export class AddonModQuizReviewPage implements OnInit { */ protected loadNavigation(): Promise { // Get all questions in single page to retrieve all the questions. - return this.quizProvider.getAttemptReview(this.attemptId, -1).then((data) => { + return this.quizProvider.getAttemptReview(this.attemptId, {page: -1, cmId: this.quiz.coursemodule}).then((data) => { const lastQuestion = data.questions[data.questions.length - 1]; data.questions.forEach((question) => { diff --git a/src/addon/mod/quiz/providers/helper.ts b/src/addon/mod/quiz/providers/helper.ts index c83293381..084cb6303 100644 --- a/src/addon/mod/quiz/providers/helper.ts +++ b/src/addon/mod/quiz/providers/helper.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { ModalController, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonModQuizProvider } from './quiz'; @@ -166,12 +166,12 @@ export class AddonModQuizHelperProvider { * Get a quiz ID by attempt ID. * * @param attemptId Attempt ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the quiz ID. */ - getQuizIdByAttemptId(attemptId: number, siteId?: string): Promise { + getQuizIdByAttemptId(attemptId: number, options: {cmId?: number, siteId?: string} = {}): Promise { // Use getAttemptReview to retrieve the quiz ID. - return this.quizProvider.getAttemptReview(attemptId, undefined, false, siteId).then((reviewData) => { + return this.quizProvider.getAttemptReview(attemptId, options).then((reviewData) => { if (reviewData.attempt && reviewData.attempt.quiz) { return reviewData.attempt.quiz; } @@ -202,7 +202,7 @@ export class AddonModQuizHelperProvider { promise = Promise.resolve(quizId); } else { // Retrieve the quiz ID using the attempt ID. - promise = this.getQuizIdByAttemptId(attemptId); + promise = this.getQuizIdByAttemptId(attemptId, {siteId}); } return promise.then((id) => { @@ -298,6 +298,11 @@ export class AddonModQuizHelperProvider { siteId?: string): Promise { const rules = accessInfo && accessInfo.activerulenames; + const modOptions = { + cmId: quiz.coursemodule, + readingStrategy: offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; let promise; if (attempt) { @@ -305,7 +310,7 @@ export class AddonModQuizHelperProvider { // We're continuing an attempt. Call getAttemptData to validate the preflight data. const page = attempt.currentpage; - promise = this.quizProvider.getAttemptData(attempt.id, page, preflightData, offline, true, siteId).then(() => { + promise = this.quizProvider.getAttemptData(attempt.id, page, preflightData, modOptions).then(() => { if (offline) { // Get current page stored in local. return this.quizOfflineProvider.getAttemptById(attempt.id).then((localAttempt) => { @@ -318,7 +323,7 @@ export class AddonModQuizHelperProvider { } else { // Attempt is overdue or finished in offline, we can only see the summary. // Call getAttemptSummary to validate the preflight data. - promise = this.quizProvider.getAttemptSummary(attempt.id, preflightData, offline, true, false, siteId); + promise = this.quizProvider.getAttemptSummary(attempt.id, preflightData, modOptions); } } else { // We're starting a new attempt, call startAttempt. diff --git a/src/addon/mod/quiz/providers/prefetch-handler.ts b/src/addon/mod/quiz/providers/prefetch-handler.ts index f1832e11d..d0847f847 100644 --- a/src/addon/mod/quiz/providers/prefetch-handler.ts +++ b/src/addon/mod/quiz/providers/prefetch-handler.ts @@ -16,10 +16,10 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; @@ -90,7 +90,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl return this.quizProvider.getQuiz(courseId, module.id).then((quiz) => { const files = this.getIntroFilesFromInstance(module, quiz); - return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true).then((attempts) => { + return this.quizProvider.getUserAttempts(quiz.id, { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }).then((attempts) => { return this.getAttemptsFeedbackFiles(quiz, attempts).then((attemptFiles) => { return files.concat(attemptFiles); }); @@ -106,9 +109,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl * * @param quiz Quiz. * @param attempts Quiz user attempts. + * @param siteId Site ID. If not defined, current site. * @return List of Files. */ - protected getAttemptsFeedbackFiles(quiz: any, attempts: any[]): Promise { + protected getAttemptsFeedbackFiles(quiz: any, attempts: any[], siteId?: string): Promise { // We have quiz data, now we'll get specific data for each attempt. const promises = []; const getInlineFiles = this.sitesProvider.getCurrentSite() && @@ -121,8 +125,11 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl const attemptGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false); if (typeof attemptGrade != 'undefined') { - promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), true) - .then((feedback) => { + promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), { + cmId: quiz.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((feedback) => { if (getInlineFiles && feedback.feedbackinlinefiles && feedback.feedbackinlinefiles.length) { files = files.concat(feedback.feedbackinlinefiles); } else if (feedback.feedbacktext && !getInlineFiles) { @@ -219,13 +226,16 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl const siteId = this.sitesProvider.getCurrentSiteId(); - return this.quizProvider.getQuiz(courseId, module.id, false, false, siteId).then((quiz) => { + return this.quizProvider.getQuiz(courseId, module.id, {siteId}).then((quiz) => { if (quiz.allowofflineattempts !== 1 || quiz.hasquestions === 0) { return false; } // Not downloadable if we reached max attempts or the quiz has an unfinished attempt. - return this.quizProvider.getUserAttempts(quiz.id, undefined, true, false, false, siteId).then((attempts) => { + return this.quizProvider.getUserAttempts(quiz.id, { + cmId: module.id, + siteId, + }).then((attempts) => { const isLastFinished = !attempts.length || this.quizProvider.isAttemptFinished(attempts[attempts.length - 1].state); return quiz.attempts === 0 || quiz.attempts > attempts.length || !isLastFinished; @@ -283,26 +293,35 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl attemptAccessInfo, preflightData; + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + // Get quiz. - return this.quizProvider.getQuiz(courseId, module.id, false, true, siteId).then((quizData) => { + return this.quizProvider.getQuiz(courseId, module.id, commonOptions).then((quizData) => { quiz = quizData; const promises = [], introFiles = this.getIntroFilesFromInstance(module, quiz); // Prefetch some quiz data. - promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { + promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, modOptions).then((info) => { quizAccessInfo = info; })); - promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId)); - promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, modOptions)); + promises.push(this.quizProvider.getUserAttempts(quiz.id, modOptions).then((atts) => { attempts = atts; - return this.getAttemptsFeedbackFiles(quiz, attempts).then((attemptFiles) => { + return this.getAttemptsFeedbackFiles(quiz, attempts, siteId).then((attemptFiles) => { return this.filepoolProvider.addFilesToQueue(siteId, attemptFiles, AddonModQuizProvider.COMPONENT, module.id); }); })); - promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId).then((info) => { + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, modOptions).then((info) => { attemptAccessInfo = info; })); @@ -338,10 +357,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl if (startAttempt) { // Re-fetch user attempts since we created a new one. - promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + promises.push(this.quizProvider.getUserAttempts(quiz.id, modOptions).then((atts) => { attempts = atts; - return this.getAttemptsFeedbackFiles(quiz, attempts).then((attemptFiles) => { + return this.getAttemptsFeedbackFiles(quiz, attempts, siteId).then((attemptFiles) => { return this.filepoolProvider.addFilesToQueue(siteId, attemptFiles, AddonModQuizProvider.COMPONENT, module.id); }); @@ -355,16 +374,16 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl } // Fetch attempt related data. - promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId)); - promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId)); + promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, modOptions)); + promises.push(this.quizProvider.getUserBestGrade(quiz.id, modOptions)); promises.push(this.quizProvider.getGradeFromGradebook(courseId, module.id, true, siteId).then((gradebookData) => { if (typeof gradebookData.graderaw != 'undefined') { - return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); + return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, modOptions); } }).catch(() => { // Ignore errors. })); - promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt. + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, modOptions)); // Last attempt. return Promise.all(promises); }).then(() => { @@ -410,23 +429,35 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl promises = [], isSequential = this.quizProvider.isNavigationSequential(quiz); + const modOptions = { + cmId: quiz.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + if (this.quizProvider.isAttemptFinished(attempt.state)) { // Attempt is finished, get feedback and review data. const attemptGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false); if (typeof attemptGrade != 'undefined') { - promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), true, siteId)); + promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), modOptions)); } // Get the review for each page. pages.forEach((page) => { - promises.push(this.quizProvider.getAttemptReview(attempt.id, page, true, siteId).catch(() => { + promises.push(this.quizProvider.getAttemptReview(attempt.id, { + page, + ...modOptions, // Include all options. + }).catch(() => { // Ignore failures, maybe the user can't review the attempt. })); }); - // Get the review for all questions in same page. - promises.push(this.quizProvider.getAttemptReview(attempt.id, -1, true, siteId).then((data) => { + // Get the review for all questions in same page. + promises.push(this.quizProvider.getAttemptReview(attempt.id, { + page: -1, + ...modOptions, // Include all options. + }).then((data) => { // Download the files inside the questions. const questionPromises = []; @@ -442,8 +473,8 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl } else { // Attempt not finished, get data needed to continue the attempt. - promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, attempt.id, false, true, siteId)); - promises.push(this.quizProvider.getAttemptSummary(attempt.id, preflightData, false, true, false, siteId)); + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, attempt.id, modOptions)); + promises.push(this.quizProvider.getAttemptSummary(attempt.id, preflightData, modOptions)); if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { // Get data for each page. @@ -453,8 +484,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl return; } - promises.push(this.quizProvider.getAttemptData(attempt.id, page, preflightData, false, true, siteId) - .then((data) => { + promises.push(this.quizProvider.getAttemptData(attempt.id, page, preflightData, modOptions).then((data) => { // Download the files inside the questions. const questionPromises = []; @@ -485,30 +515,35 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl siteId = siteId || this.sitesProvider.getCurrentSiteId(); const promises = []; + const modOptions = { + cmId: quiz.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; let attempts, quizAccessInfo, preflightData, lastAttempt; // Get quiz data. - promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { + promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, modOptions).then((info) => { quizAccessInfo = info; })); - promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId)); - promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId)); - promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId)); - promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, modOptions)); + promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, modOptions)); + promises.push(this.quizProvider.getUserBestGrade(quiz.id, modOptions)); + promises.push(this.quizProvider.getUserAttempts(quiz.id, modOptions).then((atts) => { attempts = atts; })); promises.push(this.quizProvider.getGradeFromGradebook(quiz.course, quiz.coursemodule, true, siteId) .then((gradebookData) => { if (typeof gradebookData.graderaw != 'undefined') { - return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); + return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, modOptions); } }).catch(() => { // Ignore errors. })); - promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt. + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, modOptions)); // Last attempt. return Promise.all(promises).then(() => { lastAttempt = attempts[attempts.length - 1]; @@ -529,7 +564,12 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl } }).then(() => { // Prefetch finished, set the right status. - return this.setStatusAfterPrefetch(quiz, attempts, true, false, siteId); + return this.setStatusAfterPrefetch(quiz, { + cmId: quiz.coursemodule, + attempts, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }); }); } @@ -538,29 +578,25 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl * If the last attempt is finished or there isn't one, set it as not downloaded to show download icon. * * @param quiz Quiz. - * @param attempts List of attempts. If not provided, they will be calculated. - * @param forceCache Whether it should always return cached data. Only if attempts is undefined. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). Only if - * attempts is undefined. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - setStatusAfterPrefetch(quiz: any, attempts?: any[], forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + setStatusAfterPrefetch(quiz: any, options: AddonModQuizSetStatusAfterPrefetchOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); const promises = []; let status; + let attempts = options.attempts; if (!attempts) { // Get the attempts. - promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, forceCache, ignoreCache, siteId).then((atts) => { + promises.push(this.quizProvider.getUserAttempts(quiz.id, options).then((atts) => { attempts = atts; })); } // Check the current status of the quiz. - promises.push(this.filepoolProvider.getPackageStatus(siteId, this.component, quiz.coursemodule).then((stat) => { + promises.push(this.filepoolProvider.getPackageStatus(options.siteId, this.component, quiz.coursemodule).then((stat) => { status = stat; })); @@ -573,7 +609,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl isLastFinished = !lastAttempt || this.quizProvider.isAttemptFinished(lastAttempt.state), newStatus = isLastFinished ? CoreConstants.NOT_DOWNLOADED : CoreConstants.DOWNLOADED; - return this.filepoolProvider.storePackageStatus(siteId, newStatus, this.component, quiz.coursemodule); + return this.filepoolProvider.storePackageStatus(options.siteId, newStatus, this.component, quiz.coursemodule); } }); } @@ -591,7 +627,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl this.syncProvider = this.injector.get(AddonModQuizSyncProvider); } - return this.quizProvider.getQuiz(courseId, module.id).then((quiz) => { + return this.quizProvider.getQuiz(courseId, module.id, {siteId}).then((quiz) => { return this.syncProvider.syncQuiz(quiz, false, siteId).then((results) => { module.attemptFinished = (results && results.attemptFinished) || false; @@ -604,3 +640,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl }); } } + +/** + * Options to pass to setStatusAfterPrefetch. + */ +export type AddonModQuizSetStatusAfterPrefetchOptions = CoreCourseCommonModWSOptions & { + attempts?: any[]; // List of attempts. If not provided, they will be calculated. +}; diff --git a/src/addon/mod/quiz/providers/quiz-offline.ts b/src/addon/mod/quiz/providers/quiz-offline.ts index 6c1f5121e..e7bac37d5 100644 --- a/src/addon/mod/quiz/providers/quiz-offline.ts +++ b/src/addon/mod/quiz/providers/quiz-offline.ts @@ -342,8 +342,8 @@ export class AddonModQuizOfflineProvider { for (const slot in questionsWithAnswers) { const question = questionsWithAnswers[slot]; - promises.push(this.behaviourDelegate.determineNewState( - quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT, attempt.id, question, siteId).then((state) => { + promises.push(this.behaviourDelegate.determineNewState(quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT, + attempt.id, question, quiz.coursemodule, siteId).then((state) => { // Check if state has changed. if (state && state.name != question.state) { newStates[question.slot] = state.name; diff --git a/src/addon/mod/quiz/providers/quiz-sync.ts b/src/addon/mod/quiz/providers/quiz-sync.ts index b49891a58..c4521591e 100644 --- a/src/addon/mod/quiz/providers/quiz-sync.ts +++ b/src/addon/mod/quiz/providers/quiz-sync.ts @@ -17,10 +17,11 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtils } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; @@ -77,25 +78,33 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider * @param quiz Quiz. * @param courseId Course ID. * @param warnings List of warnings generated by the sync. - * @param attemptId Last attempt ID. - * @param offlineAttempt Offline attempt synchronized, if any. - * @param onlineAttempt Online data for the offline attempt. - * @param removeAttempt Whether the offline data should be removed. - * @param updated Whether some data was sent to the site. + * @param options Other options. * @return Promise resolved on success. */ - protected finishSync(siteId: string, quiz: any, courseId: number, warnings: string[], attemptId?: number, offlineAttempt?: any, - onlineAttempt?: any, removeAttempt?: boolean, updated?: boolean): Promise { + protected finishSync(siteId: string, quiz: any, courseId: number, warnings: string[], options?: FinishSyncOptions) + : Promise { + options = options || {}; // Invalidate the data for the quiz and attempt. - return this.quizProvider.invalidateAllQuizData(quiz.id, courseId, attemptId, siteId).catch(() => { + return this.quizProvider.invalidateAllQuizData(quiz.id, courseId, options.attemptId, siteId).catch(() => { // Ignore errors. }).then(() => { - if (removeAttempt && attemptId) { - return this.quizOfflineProvider.removeAttemptAndAnswers(attemptId, siteId); + if (options.removeAttempt && options.attemptId) { + const promises = []; + + promises.push(this.quizOfflineProvider.removeAttemptAndAnswers(options.attemptId, siteId)); + + if (options.onlineQuestions) { + for (const slot in options.onlineQuestions) { + promises.push(this.questionDelegate.deleteOfflineData(options.onlineQuestions[slot], + AddonModQuizProvider.COMPONENT, quiz.coursemodule, siteId)); + } + } + + return Promise.all(promises); } }).then(() => { - if (updated) { + if (options.updated) { // Data has been sent. Update prefetched data. return this.courseProvider.getModuleBasicInfoByInstance(quiz.id, 'quiz', siteId).then((module) => { return this.prefetchAfterUpdateQuiz(module, quiz, courseId, undefined, siteId); @@ -109,14 +118,14 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider }); }).then(() => { // Check if online attempt was finished because of the sync. - if (onlineAttempt && !this.quizProvider.isAttemptFinished(onlineAttempt.state)) { + if (options.onlineAttempt && !this.quizProvider.isAttemptFinished(options.onlineAttempt.state)) { // Attempt wasn't finished at start. Check if it's finished now. - return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => { + return this.quizProvider.getUserAttempts(quiz.id, {cmId: quiz.coursemodule, siteId}).then((attempts) => { // Search the attempt. for (const i in attempts) { const attempt = attempts[i]; - if (attempt.id == onlineAttempt.id) { + if (attempt.id == options.onlineAttempt.id) { return this.quizProvider.isAttemptFinished(attempt.state); } } @@ -180,7 +189,11 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider }).then(() => { // Prefetch finished or not needed, set the right status. - return this.prefetchHandler.setStatusAfterPrefetch(quiz, undefined, shouldDownload, false, siteId); + return this.prefetchHandler.setStatusAfterPrefetch(quiz, { + cmId: module.id, + readingStrategy: shouldDownload ? CoreSitesReadingStrategy.PreferCache : undefined, + siteId, + }); }); } @@ -226,7 +239,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider if (!this.syncProvider.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) { // Quiz not blocked, try to synchronize it. - promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, false, false, siteId).then((quiz) => { + promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, {siteId}).then((quiz) => { const promise = force ? this.syncQuiz(quiz, false, siteId) : this.syncQuizIfNeeded(quiz, false, siteId); return promise.then((data) => { @@ -284,11 +297,6 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider syncQuiz(quiz: any, askPreflight?: boolean, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - const warnings = [], - courseId = quiz.course; - let syncPromise, - preflightData; - if (this.isSyncing(quiz.id, siteId)) { // There's already a sync ongoing for this quiz, return the promise. return this.getOngoingSync(quiz.id, siteId); @@ -301,112 +309,139 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); } + return this.addOngoingSync(quiz.id, this.performSyncQuiz(quiz, askPreflight, siteId), siteId); + } + + /** + * Perform the quiz sync. + * + * @param quiz Quiz. + * @param askPreflight Whether we should ask for preflight data if needed. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved in success. + */ + async performSyncQuiz(quiz: any, askPreflight?: boolean, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const warnings = []; + const courseId = quiz.course; + const modOptions = { + cmId: quiz.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + this.logger.debug('Try to sync quiz ' + quiz.id + ' in site ' + siteId); // Sync offline logs. - syncPromise = this.logHelper.syncIfNeeded(AddonModQuizProvider.COMPONENT, quiz.id, siteId).catch(() => { - // Ignore errors. - }).then(() => { - // Get all the offline attempts for the quiz. - return this.quizOfflineProvider.getQuizAttempts(quiz.id, siteId); - }).then((attempts) => { - // Should return 0 or 1 attempt. - if (!attempts.length) { - return this.finishSync(siteId, quiz, courseId, warnings); - } + await CoreUtils.instance.ignoreErrors(this.logHelper.syncIfNeeded(AddonModQuizProvider.COMPONENT, quiz.id, siteId)); - const offlineAttempt = attempts.pop(); + // Get all the offline attempts for the quiz. It should always be 0 or 1 attempt + const offlineAttempts = await this.quizOfflineProvider.getQuizAttempts(quiz.id, siteId); - // Now get the list of online attempts to make sure this attempt exists and isn't finished. - return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((attempts) => { - const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined; - let onlineAttempt; + if (!offlineAttempts.length) { + // Nothing to sync, finish. + return this.finishSync(siteId, quiz, courseId, warnings); + } - // Search the attempt we retrieved from offline. - for (const i in attempts) { - const attempt = attempts[i]; + if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + throw new Error(this.translate.instant('core.cannotconnect')); + } - if (attempt.id == offlineAttempt.id) { - onlineAttempt = attempt; - break; - } - } + const offlineAttempt = offlineAttempts.pop(); - if (!onlineAttempt || this.quizProvider.isAttemptFinished(onlineAttempt.state)) { - // Attempt not found or it's finished in online. Discard it. - warnings.push(this.translate.instant('addon.mod_quiz.warningattemptfinished')); + // Now get the list of online attempts to make sure this attempt exists and isn't finished. + const onlineAttempts = await this.quizProvider.getUserAttempts(quiz.id, modOptions); - return this.finishSync(siteId, quiz, courseId, warnings, offlineAttempt.id, offlineAttempt, onlineAttempt, - true); - } - - // Get the data stored in offline. - return this.quizOfflineProvider.getAttemptAnswers(offlineAttempt.id, siteId).then((answersList) => { - - if (!answersList.length) { - // No answers stored, finish. - return this.finishSync(siteId, quiz, courseId, warnings, lastAttemptId, offlineAttempt, onlineAttempt, - true); - } - - const answers = this.questionProvider.convertAnswersArrayToObject(answersList), - offlineQuestions = this.quizOfflineProvider.classifyAnswersInQuestions(answers); - let finish; - - // We're going to need preflightData, get it. - return this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { - - return this.prefetchHandler.getPreflightData(quiz, info, onlineAttempt, askPreflight, - 'core.settings.synchronization', siteId); - }).then((data) => { - preflightData = data; - - // Now get the online questions data. - const pages = this.quizProvider.getPagesFromLayoutAndQuestions(onlineAttempt.layout, offlineQuestions); - - return this.quizProvider.getAllQuestionsData(quiz, onlineAttempt, preflightData, pages, false, true, - siteId); - }).then((onlineQuestions) => { - - // Validate questions, discarding the offline answers that can't be synchronized. - return this.validateQuestions(onlineAttempt.id, onlineQuestions, offlineQuestions, siteId); - }).then((discardedData) => { - - // Get the answers to send. - const answers = this.quizOfflineProvider.extractAnswersFromQuestions(offlineQuestions); - finish = offlineAttempt.finished && !discardedData; - - if (discardedData) { - if (offlineAttempt.finished) { - warnings.push(this.translate.instant('addon.mod_quiz.warningdatadiscardedfromfinished')); - } else { - warnings.push(this.translate.instant('addon.mod_quiz.warningdatadiscarded')); - } - } - - return this.quizProvider.processAttempt(quiz, onlineAttempt, answers, preflightData, finish, false, false, - siteId); - }).then(() => { - - // Answers sent, now set the current page if the attempt isn't finished. - if (!finish) { - // Don't pass the quiz instance because we don't want to trigger a Firebase event in this case. - return this.quizProvider.logViewAttempt(onlineAttempt.id, offlineAttempt.currentpage, preflightData, - false, undefined, siteId).catch(() => { - // Ignore errors. - }); - } - }).then(() => { - - // Data sent. Finish the sync. - return this.finishSync(siteId, quiz, courseId, warnings, lastAttemptId, offlineAttempt, onlineAttempt, - true, true); - }); - }); - }); + const lastAttemptId = onlineAttempts.length ? onlineAttempts[onlineAttempts.length - 1].id : undefined; + const onlineAttempt = onlineAttempts.find((attempt) => { + return attempt.id == offlineAttempt.id; }); - return this.addOngoingSync(quiz.id, syncPromise, siteId); + if (!onlineAttempt || this.quizProvider.isAttemptFinished(onlineAttempt.state)) { + // Attempt not found or it's finished in online. Discard it. + warnings.push(this.translate.instant('addon.mod_quiz.warningattemptfinished')); + + return this.finishSync(siteId, quiz, courseId, warnings, { + attemptId: offlineAttempt.id, + offlineAttempt, + onlineAttempt, + removeAttempt: true, + }); + } + + // Get the data stored in offline. + const answersList = await this.quizOfflineProvider.getAttemptAnswers(offlineAttempt.id, siteId); + + if (!answersList.length) { + // No answers stored, finish. + return this.finishSync(siteId, quiz, courseId, warnings, { + attemptId: lastAttemptId, + offlineAttempt, + onlineAttempt, + removeAttempt: true, + }); + } + + const offlineAnswers = this.questionProvider.convertAnswersArrayToObject(answersList); + const offlineQuestions = this.quizOfflineProvider.classifyAnswersInQuestions(offlineAnswers); + + // We're going to need preflightData, get it. + const info = await this.quizProvider.getQuizAccessInformation(quiz.id, modOptions); + + const preflightData = await this.prefetchHandler.getPreflightData(quiz, info, onlineAttempt, askPreflight, + 'core.settings.synchronization', siteId); + + // Now get the online questions data. + const onlineQuestions = await this.quizProvider.getAllQuestionsData(quiz, onlineAttempt, preflightData, { + pages: this.quizProvider.getPagesFromLayoutAndQuestions(onlineAttempt.layout, offlineQuestions), + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); + + // Validate questions, discarding the offline answers that can't be synchronized. + const discardedData = await this.validateQuestions(onlineAttempt.id, onlineQuestions, offlineQuestions, siteId); + + // Let questions prepare the data to send. + await Promise.all(Object.keys(offlineQuestions).map(async (slot) => { + const onlineQuestion = onlineQuestions[slot]; + + await this.questionDelegate.prepareSyncData(onlineQuestion, offlineQuestions[slot].answers, + AddonModQuizProvider.COMPONENT, quiz.coursemodule, siteId); + })); + + // Get the answers to send. + const answers = this.quizOfflineProvider.extractAnswersFromQuestions(offlineQuestions); + const finish = offlineAttempt.finished && !discardedData; + + if (discardedData) { + if (offlineAttempt.finished) { + warnings.push(this.translate.instant('addon.mod_quiz.warningdatadiscardedfromfinished')); + } else { + warnings.push(this.translate.instant('addon.mod_quiz.warningdatadiscarded')); + } + } + + // Send the answers. + await this.quizProvider.processAttempt(quiz, onlineAttempt, answers, preflightData, finish, false, false, siteId); + + if (!finish) { + // Answers sent, now set the current page. + // Don't pass the quiz instance because we don't want to trigger a Firebase event in this case. + await CoreUtils.instance.ignoreErrors(this.quizProvider.logViewAttempt(onlineAttempt.id, offlineAttempt.currentpage, + preflightData, false, undefined, siteId)); + } + + // Data sent. Finish the sync. + return this.finishSync(siteId, quiz, courseId, warnings, { + attemptId: lastAttemptId, + offlineAttempt, + onlineAttempt, + removeAttempt: true, + updated: true, + onlineQuestions, + }); } /** @@ -454,3 +489,15 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider }); } } + +/** + * Options to pass to finish sync. + */ +type FinishSyncOptions = { + attemptId?: number; // Last attempt ID. + offlineAttempt?: any; // Offline attempt synchronized, if any. + onlineAttempt?: any; // Online data for the offline attempt. + removeAttempt?: boolean; // Whether the offline data should be removed. + updated?: boolean; // Whether the offline data should be removed. + onlineQuestions?: any; // Online questions indexed by slot. +}; diff --git a/src/addon/mod/quiz/providers/quiz.ts b/src/addon/mod/quiz/providers/quiz.ts index b4df1d58e..32c91fddd 100644 --- a/src/addon/mod/quiz/providers/quiz.ts +++ b/src/addon/mod/quiz/providers/quiz.ts @@ -16,18 +16,19 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate'; import { AddonModQuizOfflineProvider } from './quiz-offline'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for quiz. @@ -90,22 +91,16 @@ export class AddonModQuizProvider { * @param quiz Quiz. * @param attempt Attempt. * @param preflightData Preflight required data (like password). - * @param pages List of pages to get. If not defined, all pages. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the questions. */ - getAllQuestionsData(quiz: any, attempt: any, preflightData: any, pages?: number[], offline?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + getAllQuestionsData(quiz: any, attempt: any, preflightData: any, options: AddonModQuizAllQuestionsDataOptions = {}) + : Promise { - const promises = [], - questions = {}, - isSequential = this.isNavigationSequential(quiz); - - if (!pages) { - pages = this.getPagesFromLayout(attempt.layout); - } + const promises = []; + const questions = {}; + const isSequential = this.isNavigationSequential(quiz); + const pages = options.pages || this.getPagesFromLayout(attempt.layout); pages.forEach((page) => { if (isSequential && page < attempt.currentpage) { @@ -114,7 +109,7 @@ export class AddonModQuizProvider { } // Get the questions in the page. - promises.push(this.getAttemptData(attempt.id, page, preflightData, offline, ignoreCache, siteId).then((data) => { + promises.push(this.getAttemptData(attempt.id, page, preflightData, options).then((data) => { // Add the questions to the result object. data.questions.forEach((question) => { questions[question.slot] = question; @@ -153,29 +148,22 @@ export class AddonModQuizProvider { * * @param quizId Quiz ID. * @param attemptId Attempt ID. 0 for user's last attempt. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the access information. */ - getAttemptAccessInformation(quizId: number, attemptId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getAttemptAccessInformation(quizId: number, attemptId: number, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - quizid: quizId, - attemptid: attemptId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId) - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + attemptid: attemptId, + }; + const preSets = { + cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_attempt_access_information', params, preSets); }); @@ -208,32 +196,27 @@ export class AddonModQuizProvider { * @param attemptId Attempt ID. * @param page Page number. * @param preflightData Preflight required data (like password). - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the attempt data. */ - getAttemptData(attemptId: number, page: number, preflightData: any, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getAttemptData(attemptId: number, page: number, preflightData: any, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - attemptid: attemptId, - page: page, - preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true) - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptDataCacheKey(attemptId, page) - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + attemptid: attemptId, + page: page, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true), + }; + const preSets = { + cacheKey: this.getAttemptDataCacheKey(attemptId, page), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_attempt_data', params, preSets); + }).then((result) => { + return this.parseQuestions(result); }); } @@ -389,32 +372,28 @@ export class AddonModQuizProvider { * Get an attempt's review. * * @param attemptId Attempt ID. - * @param page Page number. If not defined, return all the questions in all the pages. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the attempt review. */ - getAttemptReview(attemptId: number, page?: number, ignoreCache?: boolean, siteId?: string): Promise { - if (typeof page == 'undefined') { - page = -1; - } + getAttemptReview(attemptId: number, options: AddonModQuizGetAttemptReviewOptions = {}): Promise { + const page = typeof options.page == 'undefined' ? -1 : options.page; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - attemptid: attemptId, - page: page - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptReviewCacheKey(attemptId, page), - cacheErrors: ['noreview'] - }; + attemptid: attemptId, + page: page, + }; + const preSets = { + cacheKey: this.getAttemptReviewCacheKey(attemptId, page), + cacheErrors: ['noreview'], + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - - return site.read('mod_quiz_get_attempt_review', params, preSets); + return site.read('mod_quiz_get_attempt_review', params, preSets).then((result) => { + return this.parseQuestions(result); + }); }); } @@ -433,34 +412,28 @@ export class AddonModQuizProvider { * * @param attemptId Attempt ID. * @param preflightData Preflight required data (like password). - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param loadLocal Whether it should load local state for each question. Only applicable if offline=true. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of questions for the attempt summary. */ - getAttemptSummary(attemptId: number, preflightData: any, offline?: boolean, ignoreCache?: boolean, loadLocal?: boolean, - siteId?: string): Promise { + getAttemptSummary(attemptId: number, preflightData: any, options: AddonModQuizGetAttemptSummaryOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - attemptid: attemptId, - preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true) - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptSummaryCacheKey(attemptId) - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + attemptid: attemptId, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true), + }; + const preSets = { + cacheKey: this.getAttemptSummaryCacheKey(attemptId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_attempt_summary', params, preSets).then((response) => { if (response && response.questions) { - if (offline && loadLocal) { + response = this.parseQuestions(response); + + if (options.loadLocal) { return this.quizOfflineProvider.loadQuestionsLocalStates(attemptId, response.questions, site.getId()); } @@ -497,27 +470,23 @@ export class AddonModQuizProvider { * Get a quiz combined review options. * * @param quizId Quiz ID. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved with the combined review options. */ - getCombinedReviewOptions(quizId: number, ignoreCache?: boolean, siteId?: string, userId?: number): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + getCombinedReviewOptions(quizId: number, options: AddonModQuizUserOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - quizid: quizId, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + userid: userId, + }; + const preSets = { + cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_combined_review_options', params, preSets).then((response) => { if (response && response.someoptions && response.alloptions) { @@ -559,25 +528,22 @@ export class AddonModQuizProvider { * * @param quizId Quiz ID. * @param grade Grade. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the feedback. */ - getFeedbackForGrade(quizId: number, grade: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getFeedbackForGrade(quizId: number, grade: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - quizid: quizId, - grade: grade - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + grade: grade, + }; + const preSets = { + cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_quiz_feedback_for_grade', params, preSets); }); @@ -683,29 +649,21 @@ export class AddonModQuizProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the Quiz is retrieved. */ - protected getQuizByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + protected getQuizByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuizDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getQuizDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModQuizProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_quizzes_by_courses', params, preSets).then((response) => { if (response && response.quizzes) { @@ -728,13 +686,11 @@ export class AddonModQuizProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the quiz is retrieved. */ - getQuiz(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.getQuizByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); + getQuiz(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getQuizByField(courseId, 'coursemodule', cmId, options); } /** @@ -742,13 +698,11 @@ export class AddonModQuizProvider { * * @param courseId Course ID. * @param id Quiz ID. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the quiz is retrieved. */ - getQuizById(courseId: number, id: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.getQuizByField(courseId, 'id', id, forceCache, ignoreCache, siteId); + getQuizById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getQuizByField(courseId, 'id', id, options); } /** @@ -765,26 +719,20 @@ export class AddonModQuizProvider { * Get access information for an attempt. * * @param quizId Quiz ID. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the access information. */ - getQuizAccessInformation(quizId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getQuizAccessInformation(quizId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - quizid: quizId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuizAccessInformationCacheKey(quizId) - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + }; + const preSets = { + cacheKey: this.getQuizAccessInformationCacheKey(quizId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_quiz_access_information', params, preSets); }); @@ -829,24 +777,21 @@ export class AddonModQuizProvider { * Get the potential question types that would be required for a given quiz. * * @param quizId Quiz ID. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the access information. */ - getQuizRequiredQtypes(quizId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getQuizRequiredQtypes(quizId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - quizid: quizId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuizRequiredQtypesCacheKey(quizId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + }; + const preSets = { + cacheKey: this.getQuizRequiredQtypesCacheKey(quizId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_quiz_required_qtypes', params, preSets).then((response) => { if (response && response.questiontypes) { @@ -981,37 +926,29 @@ export class AddonModQuizProvider { * Get quiz attempts for a certain user. * * @param quizId Quiz ID. - * @param status Status of the attempts to get. By default, 'all'. - * @param includePreviews Whether to include previews. Defaults to true. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved with the attempts. */ - getUserAttempts(quizId: number, status: string = 'all', includePreviews: boolean = true, offline?: boolean, - ignoreCache?: boolean, siteId?: string, userId?: number): Promise { + getUserAttempts(quizId: number, options: AddonModQuizGetUserAttemptsOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + const status = options.status || 'all'; + const includePreviews = typeof options.includePreviews == 'undefined' ? true : options.includePreviews; + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - quizid: quizId, - userid: userId, - status: status, - includepreviews: includePreviews ? 1 : 0 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getUserAttemptsCacheKey(quizId, userId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + userid: userId, + status: status, + includepreviews: includePreviews ? 1 : 0, + }; + const preSets = { + cacheKey: this.getUserAttemptsCacheKey(quizId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_user_attempts', params, preSets).then((response) => { if (response && response.attempts) { @@ -1048,27 +985,23 @@ export class AddonModQuizProvider { * Get best grade in a quiz for a certain user. * * @param quizId Quiz ID. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved with the best grade data. */ - getUserBestGrade(quizId: number, ignoreCache?: boolean, siteId?: string, userId?: number): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + getUserBestGrade(quizId: number, options: AddonModQuizUserOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - quizid: quizId, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getUserBestGradeCacheKey(quizId, userId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + userid: userId, + }; + const preSets = { + cacheKey: this.getUserBestGradeCacheKey(quizId, userId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_user_best_grade', params, preSets); }); @@ -1246,8 +1179,11 @@ export class AddonModQuizProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Get required data to call the invalidate functions. - return this.getQuiz(courseId, moduleId, true, false, siteId).then((quiz) => { - return this.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => { + return this.getQuiz(courseId, moduleId, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((quiz) => { + return this.getUserAttempts(quiz.id, {cmId: quiz.coursemodule, siteId}).then((attempts) => { // Now invalidate it. const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined; @@ -1630,6 +1566,26 @@ export class AddonModQuizProvider { siteId); } + /** + * Parse questions of a WS response. + * + * @param result Result to parse. + * @return Parsed result. + */ + parseQuestions(result: any): any { + for (let i = 0; i < result.questions.length; i++) { + const question = result.questions[i]; + + if (!question.settings) { + continue; + } + + question.settings = this.textUtils.parseJSON(question.settings, null); + } + + return result; + } + /** * Process an attempt, saving its data. * @@ -1703,7 +1659,12 @@ export class AddonModQuizProvider { : Promise { // Get attempt summary to have the list of questions. - return this.getAttemptSummary(attempt.id, preflightData, true, false, true, siteId).then((questionArray) => { + return this.getAttemptSummary(attempt.id, preflightData, { + cmId: quiz.coursemodule, + loadLocal: true, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((questionArray) => { // Convert the question array to an object. const questions = this.utils.arrayToObject(questionArray, 'slot'); @@ -1860,3 +1821,40 @@ export class AddonModQuizProvider { }); } } + +/** + * Common options with user ID. + */ +export type AddonModQuizUserOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User ID. If not defined use site's current user. +}; + +/** + * Options to pass to getAllQuestionsData. + */ +export type AddonModQuizAllQuestionsDataOptions = CoreCourseCommonModWSOptions & { + pages?: number[]; // List of pages to get. If not defined, all pages. +}; + +/** + * Options to pass to getAttemptReview. + */ +export type AddonModQuizGetAttemptReviewOptions = CoreCourseCommonModWSOptions & { + page?: number; // List of pages to get. If not defined, all pages. +}; + +/** + * Options to pass to getAttemptSummary. + */ +export type AddonModQuizGetAttemptSummaryOptions = CoreCourseCommonModWSOptions & { + loadLocal?: boolean; // Whether it should load local state for each question. +}; + +/** + * Options to pass to getUserAttempts. + */ +export type AddonModQuizGetUserAttemptsOptions = CoreCourseCommonModWSOptions & { + status?: string; // Status of the attempts to get. By default, 'all'. + includePreviews?: boolean; // Whether to include previews. Defaults to true. + userId?: number; // User ID. If not defined use site's current user. +}; diff --git a/src/addon/mod/resource/components/index/addon-mod-resource-index.html b/src/addon/mod/resource/components/index/addon-mod-resource-index.html index e6aa1efff..f910fabfb 100644 --- a/src/addon/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addon/mod/resource/components/index/addon-mod-resource-index.html @@ -6,7 +6,7 @@ - + diff --git a/src/addon/mod/resource/pages/index/index.html b/src/addon/mod/resource/pages/index/index.html index d52cc3f30..93155e74f 100644 --- a/src/addon/mod/resource/pages/index/index.html +++ b/src/addon/mod/resource/pages/index/index.html @@ -8,7 +8,7 @@
- + diff --git a/src/addon/mod/resource/providers/helper.ts b/src/addon/mod/resource/providers/helper.ts index 62a00e511..c1566e931 100644 --- a/src/addon/mod/resource/providers/helper.ts +++ b/src/addon/mod/resource/providers/helper.ts @@ -142,7 +142,7 @@ export class AddonModResourceHelperProvider { mimetype = this.mimetypeUtils.getMimeType(ext); } - return mimetype == 'text/html'; + return mimetype == 'text/html' || mimetype == 'application/xhtml+xml'; } /** diff --git a/src/addon/mod/resource/providers/resource.ts b/src/addon/mod/resource/providers/resource.ts index 5445ec3b1..b8b5b2fd2 100644 --- a/src/addon/mod/resource/providers/resource.ts +++ b/src/addon/mod/resource/providers/resource.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -54,18 +54,22 @@ export class AddonModResourceProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the resource is retrieved. */ - protected getResourceDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getResourceDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getResourceCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getResourceCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModResourceProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_resource_get_resources_by_courses', params, preSets) .then((response: AddonModResourceGetResourcesByCoursesResult): any => { @@ -89,11 +93,11 @@ export class AddonModResourceProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the resource is retrieved. */ - getResourceData(courseId: number, cmId: number, siteId?: string): Promise { - return this.getResourceDataByKey(courseId, 'coursemodule', cmId, siteId); + getResourceData(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getResourceDataByKey(courseId, 'coursemodule', cmId, options); } /** diff --git a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html index cc16da2c2..b3e5d565e 100644 --- a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html +++ b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/scorm/components/index/index.ts b/src/addon/mod/scorm/components/index/index.ts index e16a6484a..f6f313360 100644 --- a/src/addon/mod/scorm/components/index/index.ts +++ b/src/addon/mod/scorm/components/index/index.ts @@ -146,7 +146,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { // Get the SCORM instance. - return this.scormProvider.getScorm(this.courseId, this.module.id, this.module.url).then((scormData) => { + return this.scormProvider.getScorm(this.courseId, this.module.id, {moduleUrl: this.module.url}).then((scormData) => { this.scorm = scormData; this.dataRetrieved.emit(this.scorm); @@ -185,12 +185,12 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom const promises = []; // Get access information. - promises.push(this.scormProvider.getAccessInformation(this.scorm.id).then((accessInfo) => { + promises.push(this.scormProvider.getAccessInformation(this.scorm.id, {cmId: this.module.id}).then((accessInfo) => { this.accessInfo = accessInfo; })); // Get the number of attempts. - promises.push(this.scormProvider.getAttemptCount(this.scorm.id).then((attemptsData) => { + promises.push(this.scormProvider.getAttemptCount(this.scorm.id, {cmId: this.module.id}).then((attemptsData) => { this.attempts = attemptsData; this.hasOffline = !!this.attempts.offline.length; @@ -207,7 +207,10 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom } // Check if the last attempt is incomplete. - return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.lastAttempt, this.lastIsOffline); + return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.lastAttempt, { + offline: this.lastIsOffline, + cmId: this.module.id, + }); }).then((incomplete) => { const promises = []; @@ -260,7 +263,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom * @return Promise resolved when done. */ protected fetchStructure(): Promise { - return this.scormProvider.getOrganizations(this.scorm.id).then((organizations) => { + return this.scormProvider.getOrganizations(this.scorm.id, {cmId: this.module.id}).then((organizations) => { this.organizations = organizations; if (!this.currentOrganization.identifier) { @@ -460,8 +463,11 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom this.loadingToc = true; - return this.scormProvider.getOrganizationToc(this.scorm.id, this.lastAttempt, organizationId, this.lastIsOffline) - .then((toc) => { + return this.scormProvider.getOrganizationToc(this.scorm.id, this.lastAttempt, { + organization: organizationId, + offline: this.lastIsOffline, + cmId: this.module.id, + }).then((toc) => { this.toc = this.scormProvider.formatTocToArray(toc); diff --git a/src/addon/mod/scorm/pages/player/player.ts b/src/addon/mod/scorm/pages/player/player.ts index a21bd9e58..ed6e32de7 100644 --- a/src/addon/mod/scorm/pages/player/player.ts +++ b/src/addon/mod/scorm/pages/player/player.ts @@ -15,7 +15,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { IonicPage, NavParams, ModalController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtils } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -201,7 +201,10 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { // Check if current attempt is incomplete. if (this.attempt > 0) { - return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, this.offline); + return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, { + offline: this.offline, + cmId: this.scorm.coursemodule, + }); } else { // User doesn't have attempts. Last attempt is not incomplete (since he doesn't have any). return false; @@ -217,7 +220,10 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { return this.scormHelper.createOfflineAttempt(this.scorm, result.attempt, attemptsData.online.length); } else { // Last attempt was online, verify that we can create a new online attempt. We ignore cache. - return this.scormProvider.getScormUserData(this.scorm.id, result.attempt, undefined, false, true).catch(() => { + return this.scormProvider.getScormUserData(this.scorm.id, result.attempt, { + cmId: this.scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }).catch(() => { // Cannot communicate with the server, create an offline attempt. this.offline = true; @@ -241,18 +247,22 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { // Wait for any ongoing sync to finish. We won't sync a SCORM while it's being played. return this.scormSyncProvider.waitForSync(this.scorm.id).then(() => { // Get attempts data. - return this.scormProvider.getAttemptCount(this.scorm.id).then((attemptsData) => { + return this.scormProvider.getAttemptCount(this.scorm.id, {cmId: this.scorm.coursemodule}).then((attemptsData) => { return this.determineAttemptAndMode(attemptsData).then(() => { // Fetch TOC and get user data. const promises = []; promises.push(this.fetchToc()); - promises.push(this.scormProvider.getScormUserData(this.scorm.id, this.attempt, undefined, this.offline) - .then((data) => { + promises.push(this.scormProvider.getScormUserData(this.scorm.id, this.attempt, { + cmId: this.scorm.coursemodule, + offline: this.offline, + }).then((data) => { this.userData = data; })); // Get access information. - promises.push(this.scormProvider.getAccessInformation(this.scorm.id).then((accessInfo) => { + promises.push(this.scormProvider.getAccessInformation(this.scorm.id, { + cmId: this.scorm.coursemodule, + }).then((accessInfo) => { this.accessInfo = accessInfo; })); @@ -273,11 +283,18 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.loadingToc = true; // We need to check incomplete again: attempt number or status might have changed. - return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, this.offline).then((incomplete) => { + return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, { + offline: this.offline, + cmId: this.scorm.coursemodule, + }).then((incomplete) => { this.scorm.incomplete = incomplete; // Get TOC. - return this.scormProvider.getOrganizationToc(this.scorm.id, this.attempt, this.organizationId, this.offline); + return this.scormProvider.getOrganizationToc(this.scorm.id, this.attempt, { + organization: this.organizationId, + offline: this.offline, + cmId: this.scorm.coursemodule, + }); }).then((toc) => { this.toc = this.scormProvider.formatTocToArray(toc); @@ -300,8 +317,13 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { if (!this.currentSco) { // No SCO defined. Get the first valid one. - return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, this.toc, this.organizationId, this.mode, - this.offline).then((sco) => { + return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, { + toc: this.toc, + organization: this.organizationId, + mode: this.mode, + offline: this.offline, + cmId: this.scorm.coursemodule, + }).then((sco) => { if (sco) { this.currentSco = sco; @@ -374,7 +396,9 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.scormProvider.saveTracks(sco.id, this.attempt, tracks, this.scorm, this.offline).catch(() => { // Error saving data. We'll go offline if we're online and the asset is not marked as completed already. if (!this.offline) { - return this.scormProvider.getScormUserData(this.scorm.id, this.attempt, undefined, false).then((data) => { + return this.scormProvider.getScormUserData(this.scorm.id, this.attempt, { + cmId: this.scorm.coursemodule, + }).then((data) => { if (!data[sco.id] || data[sco.id].userdata['cmi.core.lesson_status'] != 'completed') { // Go offline. return this.scormHelper.convertAttemptToOffline(this.scorm, this.attempt).then(() => { @@ -462,7 +486,10 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { return this.scormProvider.saveTracks(scoId, this.attempt, tracks, this.scorm, this.offline).then(() => { if (!this.offline) { // New online attempt created, update cached data about online attempts. - this.scormProvider.getAttemptCount(this.scorm.id, false, true).catch(() => { + this.scormProvider.getAttemptCount(this.scorm.id, { + cmId: this.scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }).catch(() => { // Ignore errors. }); } diff --git a/src/addon/mod/scorm/providers/helper.ts b/src/addon/mod/scorm/providers/helper.ts index 49fe8f6fd..c838894f1 100644 --- a/src/addon/mod/scorm/providers/helper.ts +++ b/src/addon/mod/scorm/providers/helper.ts @@ -19,6 +19,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm'; import { AddonModScormOfflineProvider } from './scorm-offline'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Helper service that provides some features for SCORM. @@ -78,7 +79,7 @@ export class AddonModScormHelperProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Get data from the online attempt. - return this.scormProvider.getScormUserData(scorm.id, attempt, undefined, false, false, siteId).then((onlineData) => { + return this.scormProvider.getScormUserData(scorm.id, attempt, {cmId: scorm.coursemodule, siteId}).then((onlineData) => { // The SCORM API might have written some data to the offline attempt already. // We don't want to override it with cached online data. return this.scormOfflineProvider.getScormUserData(scorm.id, attempt, undefined, siteId).catch(() => { @@ -131,7 +132,7 @@ export class AddonModScormHelperProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Try to get data from online attempts. - return this.searchOnlineAttemptUserData(scorm.id, lastOnline, siteId).then((userData) => { + return this.searchOnlineAttemptUserData(scorm.id, lastOnline, {cmId: scorm.coursemodule, siteId}).then((userData) => { // We're creating a new attempt, remove all the user data that is not needed for a new attempt. for (const scoId in userData) { const sco = userData[scoId], @@ -177,7 +178,11 @@ export class AddonModScormHelperProvider { // Check if last online incomplete. const hasOffline = attempts.offline.indexOf(lastOnline) > -1; - return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, hasOffline, false, siteId).then((incomplete) => { + return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, { + offline: hasOffline, + cmId: scorm.coursemodule, + siteId, + }).then((incomplete) => { if (incomplete) { return { number: lastOnline, @@ -197,24 +202,19 @@ export class AddonModScormHelperProvider { * * @param scormId Scorm ID. * @param attempt Attempt number. - * @param toc SCORM's TOC. If not provided, it will be calculated. - * @param organization Organization to use. - * @param mode Mode. - * @param offline Whether the attempt is offline. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the first SCO. */ - getFirstSco(scormId: number, attempt: number, toc?: any[], organization?: string, mode?: string, offline?: boolean, - siteId?: string): Promise { + getFirstSco(scormId: number, attempt: number, options: AddonModScormGetFirstScoOptions = {}): Promise { - mode = mode || AddonModScormProvider.MODENORMAL; + const mode = options.mode || AddonModScormProvider.MODENORMAL; let promise; - if (toc && toc.length) { - promise = Promise.resolve(toc); + if (options.toc && options.toc.length) { + promise = Promise.resolve(options.toc); } else { // SCORM doesn't have a TOC. Get all the scos. - promise = this.scormProvider.getScosWithData(scormId, attempt, organization, offline, false, siteId); + promise = this.scormProvider.getScosWithData(scormId, attempt, options); } return promise.then((scos) => { @@ -319,16 +319,16 @@ export class AddonModScormHelperProvider { * * @param scormId SCORM ID. * @param attempt Online attempt to get the data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with user data. */ - searchOnlineAttemptUserData(scormId: number, attempt: number, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + searchOnlineAttemptUserData(scormId: number, attempt: number, options: CoreCourseCommonModWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, false, siteId).catch(() => { + return this.scormProvider.getScormUserData(scormId, attempt, options).catch(() => { if (attempt > 0) { // We couldn't retrieve the data. Try again with the previous online attempt. - return this.searchOnlineAttemptUserData(scormId, attempt - 1, siteId); + return this.searchOnlineAttemptUserData(scormId, attempt - 1, options); } else { // No more attempts to try. Reject return Promise.reject(null); @@ -336,3 +336,13 @@ export class AddonModScormHelperProvider { }); } } + +/** + * Options to pass to getFirstSco. + */ +export type AddonModScormGetFirstScoOptions = CoreCourseCommonModWSOptions & { + toc?: any[]; // SCORM's TOC. If not provided, it will be calculated. + organization?: string; // Organization to use. + mode?: string; // Mode. + offline?: boolean; // Whether the attempt is offline. +}; diff --git a/src/addon/mod/scorm/providers/prefetch-handler.ts b/src/addon/mod/scorm/providers/prefetch-handler.ts index d8263dbd7..376329f9d 100644 --- a/src/addon/mod/scorm/providers/prefetch-handler.ts +++ b/src/addon/mod/scorm/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -111,7 +111,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand let scorm; - return this.scormProvider.getScorm(courseId, module.id, module.url, false, siteId).then((scormData) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url, siteId}).then((scormData) => { scorm = scormData; const promises = [], @@ -132,9 +132,6 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand // Ignore errors. })); - // Prefetch access information. - promises.push(this.scormProvider.getAccessInformation(scorm.id)); - return Promise.all(promises); }).then(() => { // Success, return the hash. @@ -246,9 +243,14 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand siteId = siteId || this.sitesProvider.getCurrentSiteId(); const promises = []; + const modOptions = { + cmId: scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; // Prefetch number of attempts (including not completed). - promises.push(this.scormProvider.getAttemptCountOnline(scorm.id, undefined, true, siteId).catch(() => { + promises.push(this.scormProvider.getAttemptCountOnline(scorm.id, modOptions).catch(() => { // If it fails, assume we have no attempts. return 0; }).then((numAttempts) => { @@ -257,7 +259,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand const dataPromises = []; for (let i = 1; i <= numAttempts; i++) { - dataPromises.push(this.scormProvider.getScormUserDataOnline(scorm.id, i, true, siteId).catch((err) => { + dataPromises.push(this.scormProvider.getScormUserDataOnline(scorm.id, i, modOptions).catch((err) => { // Ignore failures of all the attempts that aren't the last one. if (i == numAttempts) { return Promise.reject(err); @@ -268,12 +270,15 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand return Promise.all(dataPromises); } else { // No attempts. We'll still try to get user data to be able to identify SCOs not visible and so. - return this.scormProvider.getScormUserDataOnline(scorm.id, 0, true, siteId); + return this.scormProvider.getScormUserDataOnline(scorm.id, 0, modOptions); } })); // Prefetch SCOs. - promises.push(this.scormProvider.getScos(scorm.id, undefined, true, siteId)); + promises.push(this.scormProvider.getScos(scorm.id, modOptions)); + + // Prefetch access information. + promises.push(this.scormProvider.getAccessInformation(scorm.id, modOptions)); return Promise.all(promises); } @@ -288,7 +293,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand * to calculate the total size. */ getDownloadSize(module: any, courseId: any, single?: boolean): Promise<{ size: number, total: boolean }> { - return this.scormProvider.getScorm(courseId, module.id, module.url).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url}).then((scorm) => { if (this.scormProvider.isScormUnsupported(scorm)) { return {size: -1, total: false}; } else if (!scorm.packagesize) { @@ -310,7 +315,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Size, or promise resolved with the size. */ getDownloadedSize(module: any, courseId: number): number | Promise { - return this.scormProvider.getScorm(courseId, module.id, module.url).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url}).then((scorm) => { // Get the folder where SCORM should be unzipped. return this.scormProvider.getScormFolder(scorm.moduleurl); }).then((path) => { @@ -327,7 +332,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Promise resolved with the list of files. */ getFiles(module: any, courseId: number, single?: boolean): Promise { - return this.scormProvider.getScorm(courseId, module.id, module.url).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url}).then((scorm) => { return this.scormProvider.getScormFileList(scorm); }).catch(() => { // SCORM not found, return empty list. @@ -366,7 +371,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Whether the module can be downloaded. The promise should never be rejected. */ isDownloadable(module: any, courseId: number): boolean | Promise { - return this.scormProvider.getScorm(courseId, module.id, module.url).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url}).then((scorm) => { if (scorm.warningMessage) { // SCORM closed or not opened yet. return false; @@ -409,7 +414,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand const siteId = this.sitesProvider.getCurrentSiteId(); let scorm; - return this.scormProvider.getScorm(courseId, module.id, module.url, false, siteId).then((scormData) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url, siteId}).then((scormData) => { scorm = scormData; // Get the folder where SCORM should be unzipped. @@ -419,17 +424,15 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand // Remove the unzipped folder. promises.push(this.fileProvider.removeDir(path).catch((error) => { - if (error && error.code == 1) { + if (error && (error.code == 1 || !this.appProvider.isMobile())) { // Not found, ignore error. } else { return Promise.reject(error); } })); - // Maybe the ZIP wasn't deleted for some reason. Try to delete it too. - promises.push(this.filepoolProvider.removeFileByUrl(siteId, this.scormProvider.getPackageUrl(scorm)).catch(() => { - // Ignore errors. - })); + // Delete other files. + promises.push(this.filepoolProvider.removeFilesByComponent(siteId, this.component, module.id)); return Promise.all(promises); }); @@ -448,7 +451,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand this.syncProvider = this.injector.get(AddonModScormSyncProvider); } - return this.scormProvider.getScorm(courseId, module.id, module.url, false, siteId).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url, siteId}).then((scorm) => { return this.syncProvider.syncScorm(scorm, siteId); }); } diff --git a/src/addon/mod/scorm/providers/scorm-sync.ts b/src/addon/mod/scorm/providers/scorm-sync.ts index 59ecbb813..aba64e427 100644 --- a/src/addon/mod/scorm/providers/scorm-sync.ts +++ b/src/addon/mod/scorm/providers/scorm-sync.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -129,14 +129,20 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * @param scormId SCORM ID. * @param attempt Attempt number. * @param lastOnline Last online attempt number. + * @param cmId Module ID. * @param siteId Site ID. * @return Promise resolved if can retry the synchronization, rejected otherwise. */ - protected canRetrySync(scormId: number, attempt: number, lastOnline: number, siteId: string): Promise { + protected canRetrySync(scormId: number, attempt: number, lastOnline: number, cmId: number, siteId: string): Promise { + // If it's the last attempt we don't need to ignore cache because we already did it. const refresh = lastOnline != attempt; - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, refresh, siteId).then((siteData) => { + return this.scormProvider.getScormUserData(scormId, attempt, { + cmId, + readingStrategy: refresh ? CoreSitesReadingStrategy.OnlyNetwork : undefined, + siteId, + }).then((siteData) => { // Get synchronization snapshot (if sync fails it should store a snapshot). return this.scormOfflineProvider.getAttemptSnapshot(scormId, attempt, siteId).then((snapshot) => { if (!snapshot || !Object.keys(snapshot).length || !this.snapshotEquals(snapshot, siteData)) { @@ -209,12 +215,16 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide // Check if an attempt was finished in Moodle. if (initialCount) { // Get attempt count again to check if an attempt was finished. - return this.scormProvider.getAttemptCount(scorm.id, undefined, false, siteId).then((attemptsData) => { + return this.scormProvider.getAttemptCount(scorm.id, {cmId: scorm.coursemodule, siteId}).then((attemptsData) => { if (attemptsData.online.length > initialCount.online.length) { return true; } else if (!lastOnlineWasFinished && lastOnline > 0) { // Last online attempt wasn't finished, let's check if it is now. - return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, false, true, siteId).then((inc) => { + return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, { + cmId: scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((inc) => { return !inc; }); } @@ -238,14 +248,19 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * * @param scormId SCORM ID. * @param attempt Attempt number. + * @param cmId Module ID. * @param siteId Site ID. * @return Promise resolved with the data. */ - protected getOfflineAttemptData(scormId: number, attempt: number, siteId: string) + protected getOfflineAttemptData(scormId: number, attempt: number, cmId: number, siteId: string) : Promise<{incomplete: boolean, timecreated: number}> { // Check if last offline attempt is incomplete. - return this.scormProvider.isAttemptIncomplete(scormId, attempt, true, false, siteId).then((incomplete) => { + return this.scormProvider.isAttemptIncomplete(scormId, attempt, { + offline: true, + cmId, + siteId, + }).then((incomplete) => { return this.scormOfflineProvider.getAttemptCreationTime(scormId, attempt, siteId).then((timecreated) => { return { incomplete: incomplete, @@ -363,17 +378,22 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * * @param scormId SCORM ID. * @param attempt Attemot number. + * @param cmId Module ID. * @param siteId Site ID. * @return Promise resolved when the snapshot is stored. */ - protected saveSyncSnapshot(scormId: number, attempt: number, siteId: string): Promise { + protected saveSyncSnapshot(scormId: number, attempt: number, cmId: number, siteId: string): Promise { // Try to get current state from the site. - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, true, siteId).then((data) => { + return this.scormProvider.getScormUserData(scormId, attempt, { + cmId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((data) => { return this.scormOfflineProvider.setAttemptSnapshot(scormId, attempt, data, siteId); }, () => { // Error getting user data from the site. We'll have to build it ourselves. // Let's try to get cached data about the attempt. - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, false, siteId).catch(() => { + return this.scormProvider.getScormUserData(scormId, attempt, {cmId, siteId}).catch(() => { // No cached data. return {}; }).then((data) => { @@ -479,7 +499,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide scorms.forEach((scorm) => { if (!this.syncProvider.isBlocked(AddonModScormProvider.COMPONENT, scorm.id, siteId)) { - promises.push(this.scormProvider.getScormById(scorm.courseId, scorm.id, '', false, siteId).then((scorm) => { + promises.push(this.scormProvider.getScormById(scorm.courseId, scorm.id, {siteId}).then((scorm) => { const promise = force ? this.syncScorm(scorm, siteId) : this.syncScormIfNeeded(scorm, siteId); return promise.then((data) => { @@ -506,10 +526,11 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * * @param scormId SCORM ID. * @param attempt Attempt number. + * @param cmId Module ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the attempt is successfully synced. */ - protected syncAttempt(scormId: number, attempt: number, siteId?: string): Promise { + protected syncAttempt(scormId: number, attempt: number, cmId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); this.logger.debug('Try to sync attempt ' + attempt + ' in SCORM ' + scormId + ' and site ' + siteId); @@ -565,7 +586,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide this.logger.error('Error synchronizing some SCOs for attempt ' + attempt + ' in SCORM ' + scormId + '. Saving snapshot.'); - return this.saveSyncSnapshot(scormId, attempt, siteId).then(() => { + return this.saveSyncSnapshot(scormId, attempt, cmId, siteId).then(() => { return Promise.reject(error); }); } else { @@ -629,7 +650,11 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide // Ignore errors. }).then(() => { // Get attempts data. We ignore cache for online attempts, so this call will fail if offline or server down. - return this.scormProvider.getAttemptCount(scorm.id, false, true, siteId); + return this.scormProvider.getAttemptCount(scorm.id, { + cmId: scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); }).then((attemptsData) => { if (!attemptsData.offline || !attemptsData.offline.length) { // Nothing to sync. @@ -649,8 +674,12 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide }); // Check if last online attempt is finished. Ignore cache. - const promise = lastOnline > 0 ? this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, false, true, siteId) : - Promise.resolve(false); + const promise = lastOnline <= 0 ? Promise.resolve(false) : + this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, { + cmId: scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); return promise.then((incomplete) => { lastOnlineWasFinished = !incomplete; @@ -661,7 +690,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide attemptsData.offline.forEach((attempt) => { if (scorm.maxattempt == 0 || attempt <= scorm.maxattempt) { - promises.push(this.syncAttempt(scorm.id, attempt, siteId)); + promises.push(this.syncAttempt(scorm.id, attempt, scorm.coursemodule, siteId)); } }); @@ -672,7 +701,8 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide } else if (collisions.length) { // We have collisions, treat them. - return this.treatCollisions(scorm.id, collisions, lastOnline, attemptsData.offline, siteId).then((warns) => { + return this.treatCollisions(scorm.id, collisions, lastOnline, attemptsData.offline, scorm.coursemodule, siteId) + .then((warns) => { warnings = warnings.concat(warns); // The offline attempts might have changed since some collisions can be converted to new attempts. @@ -694,7 +724,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide // We'll only sync new attemps if last online attempt is completed. if (!incomplete || attempt <= lastOnline) { if (scorm.maxattempt == 0 || attempt <= scorm.maxattempt) { - promises.push(this.syncAttempt(scorm.id, attempt, siteId)); + promises.push(this.syncAttempt(scorm.id, attempt, scorm.coursemodule, siteId)); } } else { cannotSyncSome = true; @@ -730,6 +760,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * @param collisions Numbers of attempts that exist both in online and offline. * @param lastOnline Last online attempt. * @param offlineAttempts Numbers of offline attempts. + * @param cmId Module ID. * @param siteId Site ID. * @return Promise resolved when the collisions have been treated. It returns warnings array. * @description @@ -751,8 +782,8 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * of the list if the last attempt is completed. If the last attempt is not completed then the offline data will de deleted * because we can't create a new attempt. */ - protected treatCollisions(scormId: number, collisions: number[], lastOnline: number, offlineAttempts: number[], siteId: string) - : Promise { + protected treatCollisions(scormId: number, collisions: number[], lastOnline: number, offlineAttempts: number[], cmId: number, + siteId: string): Promise { const warnings = [], newAttemptsSameOrder = [], // Attempts that will be created as new attempts but keeping the current order. @@ -761,7 +792,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide let lastOffline = Math.max.apply(Math, offlineAttempts); // Get needed data from the last offline attempt. - return this.getOfflineAttemptData(scormId, lastOffline, siteId).then((lastOfflineData) => { + return this.getOfflineAttemptData(scormId, lastOffline, cmId, siteId).then((lastOfflineData) => { const promises = []; collisions.forEach((attempt) => { @@ -785,7 +816,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide if (hasDataToSend) { // There are elements to sync. We need to check if it's possible to sync them or not. - return this.canRetrySync(scormId, attempt, lastOnline, siteId).catch(() => { + return this.canRetrySync(scormId, attempt, lastOnline, cmId, siteId).catch(() => { // Cannot retry sync, we'll create a new offline attempt if possible. return this.addToNewOrDelete(scormId, attempt, lastOffline, newAttemptsSameOrder, newAttemptsAtEnd, lastOfflineData.timecreated, lastOfflineData.incomplete, warnings, @@ -806,8 +837,11 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide // If it's the last attempt we don't need to ignore cache because we already did it. const refresh = lastOnline != attempt; - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, refresh, siteId) - .then((data) => { + return this.scormProvider.getScormUserData(scormId, attempt, { + cmId, + readingStrategy: refresh ? CoreSitesReadingStrategy.OnlyNetwork : undefined, + siteId, + }).then((data) => { if (!this.snapshotEquals(snapshot, data)) { // Snapshot has diverged, it will be converted into a new attempt if possible. diff --git a/src/addon/mod/scorm/providers/scorm.ts b/src/addon/mod/scorm/providers/scorm.ts index 2cec079ec..da74ecc02 100644 --- a/src/addon/mod/scorm/providers/scorm.ts +++ b/src/addon/mod/scorm/providers/scorm.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreWSProvider } from '@providers/ws'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -25,9 +25,10 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUrlUtils } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonModScormOfflineProvider } from './scorm-offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreConstants } from '@core/constants'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Result of getAttemptCount. @@ -463,24 +464,25 @@ export class AddonModScormProvider { * Get access information for a given SCORM. * * @param scormId SCORM ID. - * @param forceCache True to always get the value from cache. false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Object with access information. * @since 3.7 */ - getAccessInformation(scormId: number, forceCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAccessInformation(scormId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { if (!site.wsAvailable('mod_scorm_get_scorm_access_information')) { // Access information not available for 3.6 or older sites. return Promise.resolve({}); } const params = { - scormid: scormId + scormid: scormId, }; const preSets = { cacheKey: this.getAccessInformationCacheKey(scormId), - omitExpires: forceCache + component: AddonModScormProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_scorm_get_scorm_access_information', params, preSets); @@ -501,19 +503,15 @@ export class AddonModScormProvider { * Get the number of attempts done by a user in the given SCORM. * * @param scormId SCORM ID. - * @param ignoreMissing Whether it should ignore attempts without grade/completion. Only for online attempts. - * @param ignoreCache Whether it should ignore cached data for online attempts. - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved when done. */ - getAttemptCount(scormId: number, ignoreMissing?: boolean, ignoreCache?: boolean, siteId?: string, userId?: number) - : Promise { + getAttemptCount(scormId: number, options: AddonModScormGetAttemptCountOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const result: AddonModScormAttemptCountResult = { lastAttempt: { @@ -523,7 +521,7 @@ export class AddonModScormProvider { }, promises = []; - promises.push(this.getAttemptCountOnline(scormId, ignoreMissing, ignoreCache, siteId, userId).then((count) => { + promises.push(this.getAttemptCountOnline(scormId, options).then((count) => { // Calculate numbers of online attempts. result.online = []; @@ -539,7 +537,7 @@ export class AddonModScormProvider { } })); - promises.push(this.scormOfflineProvider.getAttempts(scormId, siteId, userId).then((attempts) => { + promises.push(this.scormOfflineProvider.getAttempts(scormId, options.siteId, userId).then((attempts) => { // Get only attempt numbers. result.offline = attempts.map((entry) => { // Calculate last attempt. We use >= to prioritize offline events if an attempt is both online and offline. @@ -584,32 +582,26 @@ export class AddonModScormProvider { * Get the number of attempts done by a user in the given SCORM in online. * * @param scormId SCORM ID. - * @param ignoreMissing Whether it should ignore attempts that haven't reported a grade/completion. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved when the attempt count is retrieved. */ - getAttemptCountOnline(scormId: number, ignoreMissing?: boolean, ignoreCache?: boolean, siteId?: string, userId?: number) - : Promise { + getAttemptCountOnline(scormId: number, options: AddonModScormGetAttemptCountOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - scormid: scormId, - userid: userId, - ignoremissingcompletion: ignoreMissing ? 1 : 0 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptCountCacheKey(scormId, userId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + scormid: scormId, + userid: userId, + ignoremissingcompletion: options.ignoreMissing ? 1 : 0, + }; + const preSets = { + cacheKey: this.getAttemptCountCacheKey(scormId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModScormProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_scorm_get_scorm_attempt_count', params, preSets).then((response) => { if (response && typeof response.attemptscount != 'undefined') { @@ -640,7 +632,7 @@ export class AddonModScormProvider { }; // Get the user data and use it to calculate the grade. - return this.getScormUserData(scorm.id, attempt, undefined, offline, false, siteId).then((data) => { + return this.getScormUserData(scorm.id, attempt, {offline, cmId: scorm.coursemodule, siteId}).then((data) => { for (const scoId in data) { const sco = data[scoId], userData = sco.userdata; @@ -694,11 +686,11 @@ export class AddonModScormProvider { * Get the list of a organizations defined in a SCORM package. * * @param scormId SCORM ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of organizations. */ - getOrganizations(scormId: number, siteId?: string): Promise { - return this.getScos(scormId, undefined, false, siteId).then((scos) => { + getOrganizations(scormId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.getScos(scormId, options).then((scos) => { const organizations = []; scos.forEach((sco) => { @@ -721,15 +713,12 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt The attempt number (to populate SCO track data). - * @param organization Organization identifier. - * @param offline Whether the attempt is offline. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the toc object. */ - getOrganizationToc(scormId: number, attempt: number, organization?: string, offline?: boolean, siteId?: string) - : Promise { + getOrganizationToc(scormId: number, attempt: number, options: AddonModScormGetScosWithDataOptions = {}): Promise { - return this.getScosWithData(scormId, attempt, organization, offline, false, siteId).then((scos) => { + return this.getScosWithData(scormId, attempt, options).then((scos) => { const map = {}, rootScos = []; @@ -738,7 +727,7 @@ export class AddonModScormProvider { map[sco.identifier] = index; if (sco.parent !== '/') { - if (sco.parent == organization) { + if (sco.parent == options.organization) { // It's a root SCO, add it to the root array. rootScos.push(sco); } else { @@ -774,26 +763,22 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt Attempt number. - * @param scos SCOs returned by getScos. Recommended if offline=true. - * @param offline Whether the attempt is offline. - * @param ignoreCache Whether it should ignore cached data for online attempts. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the user data is retrieved. */ - getScormUserData(scormId: number, attempt: number, scos?: any[], offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getScormUserData(scormId: number, attempt: number, options: AddonModScormGetUserDataOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - if (offline) { + if (options.offline) { // Get SCOs if not provided. - const promise = scos ? Promise.resolve(scos) : this.getScos(scormId, undefined, undefined, siteId); + const promise = options.scos ? Promise.resolve(options.scos) : this.getScos(scormId, options); return promise.then((scos) => { - return this.scormOfflineProvider.getScormUserData(scormId, attempt, scos, siteId); + return this.scormOfflineProvider.getScormUserData(scormId, attempt, scos, options.siteId); }); } else { - return this.getScormUserDataOnline(scormId, attempt, ignoreCache, siteId); + return this.getScormUserDataOnline(scormId, attempt, options); } } @@ -823,24 +808,21 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt Attempt number. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the user data is retrieved. */ - getScormUserDataOnline(scormId: number, attempt: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getScormUserDataOnline(scormId: number, attempt: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - scormid: scormId, - attempt: attempt - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getScormUserDataCacheKey(scormId, attempt) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + scormid: scormId, + attempt: attempt, + }; + const preSets = { + cacheKey: this.getScormUserDataCacheKey(scormId, attempt), + component: AddonModScormProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_scorm_get_scorm_user_data', params, preSets).then((response) => { if (response && response.data) { @@ -876,37 +858,33 @@ export class AddonModScormProvider { * Retrieves the list of SCO objects for a given SCORM and organization. * * @param scormId SCORM ID. - * @param organization Organization. - * @param ignoreCache Whether it should ignore cached data (it will always fail if offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with a list of SCO. */ - getScos(scormId: number, organization?: string, ignoreCache?: boolean, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getScos(scormId: number, options: AddonModScormOrganizationOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { // Don't send the organization to the WS, we'll filter them locally. const params = { - scormid: scormId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getScosCacheKey(scormId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + scormid: scormId, + }; + const preSets = { + cacheKey: this.getScosCacheKey(scormId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModScormProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_scorm_get_scorm_scoes', params, preSets).then((response) => { if (response && response.scoes) { - if (organization) { + if (options.organization) { // Filter SCOs by organization. return response.scoes.filter((sco) => { - return sco.organization == organization; + return sco.organization == options.organization; }); } else { return response.scoes; @@ -924,20 +902,21 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt Attempt number. - * @param organization Organization ID. - * @param offline Whether the attempt is offline. - * @param ignoreCache Whether it should ignore cached data for online attempts. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with a list of SCO objects. */ - getScosWithData(scormId: number, attempt: number, organization?: string, offline?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + getScosWithData(scormId: number, attempt: number, options: AddonModScormGetScosWithDataOptions = {}): Promise { // Get organization SCOs. - return this.getScos(scormId, organization, ignoreCache, siteId).then((scos) => { + return this.getScos(scormId, options).then((scos) => { // Get the track data for all the SCOs in the organization for the given attempt. // We'll use this data to set SCO data like isvisible, status and so. - return this.getScormUserData(scormId, attempt, scos, offline, ignoreCache, siteId).then((data) => { + const userDataOptions = { + scos, + ...options, // Include all options. + }; + + return this.getScormUserData(scormId, attempt, userDataOptions).then((data) => { const trackDataBySCO = {}; @@ -1134,26 +1113,22 @@ export class AddonModScormProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param moduleUrl Module URL. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the SCORM is retrieved. */ - protected getScormByField(courseId: number, key: string, value: any, moduleUrl?: string, forceCache?: boolean, siteId?: string) + protected getScormByField(courseId: number, key: string, value: any, options: AddonModScormGetScormOptions = {}) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getScormDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getScormDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModScormProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_scorm_get_scorms_by_courses', params, preSets).then((response) => { if (response && response.scorms) { @@ -1173,7 +1148,7 @@ export class AddonModScormProvider { } } - currentScorm.moduleurl = moduleUrl; + currentScorm.moduleurl = options.moduleUrl; return currentScorm; } @@ -1189,13 +1164,11 @@ export class AddonModScormProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param moduleUrl Module URL. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the SCORM is retrieved. */ - getScorm(courseId: number, cmId: number, moduleUrl?: string, forceCache?: boolean, siteId?: string): Promise { - return this.getScormByField(courseId, 'coursemodule', cmId, moduleUrl, forceCache, siteId); + getScorm(courseId: number, cmId: number, options: AddonModScormGetScormOptions = {}): Promise { + return this.getScormByField(courseId, 'coursemodule', cmId, options); } /** @@ -1203,13 +1176,11 @@ export class AddonModScormProvider { * * @param courseId Course ID. * @param id SCORM ID. - * @param moduleUrl Module URL. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the SCORM is retrieved. */ - getScormById(courseId: number, id: number, moduleUrl?: string, forceCache?: boolean, siteId?: string): Promise { - return this.getScormByField(courseId, 'id', id, moduleUrl, forceCache, siteId); + getScormById(courseId: number, id: number, options: AddonModScormGetScormOptions = {}): Promise { + return this.getScormByField(courseId, 'id', id, options); } /** @@ -1314,7 +1285,7 @@ export class AddonModScormProvider { invalidateContent(moduleId: number, courseId: number, siteId?: string, userId?: number): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.getScorm(courseId, moduleId, undefined, false, siteId).then((scorm) => { + return this.getScorm(courseId, moduleId, {siteId}).then((scorm) => { const promises = []; promises.push(this.invalidateAllScormData(scorm.id, siteId, userId)); @@ -1369,15 +1340,12 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt Attempt. - * @param offline Whether the attempt is offline. - * @param ignoreCache Whether it should ignore cached data for online attempts. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with a boolean: true if incomplete, false otherwise. */ - isAttemptIncomplete(scormId: number, attempt: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + isAttemptIncomplete(scormId: number, attempt: number, options: AddonModScormOfflineOptions = {}): Promise { - return this.getScosWithData(scormId, attempt, undefined, offline, ignoreCache, siteId).then((scos) => { + return this.getScosWithData(scormId, attempt, options).then((scos) => { for (const i in scos) { const sco = scos[i]; @@ -1563,8 +1531,8 @@ export class AddonModScormProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (offline) { - const promise = userData ? Promise.resolve(userData) : this.getScormUserData(scorm.id, attempt, undefined, offline, - false, siteId); + const promise = userData ? Promise.resolve(userData) : + this.getScormUserData(scorm.id, attempt, {offline, cmId: scorm.coursemodule, siteId}); return promise.then((userData) => { return this.scormOfflineProvider.saveTracks(scorm, scoId, attempt, tracks, userData, siteId); @@ -1572,7 +1540,7 @@ export class AddonModScormProvider { } else { return this.saveTracksOnline(scorm.id, scoId, attempt, tracks, siteId).then(() => { // Tracks have been saved, update cached user data. - this.updateUserDataAfterSave(scorm.id, attempt, tracks, siteId); + this.updateUserDataAfterSave(scorm.id, attempt, tracks, {cmId: scorm.coursemodule, siteId}); this.eventsProvider.trigger(AddonModScormProvider.DATA_SENT_EVENT, { scormId: scorm.id, @@ -1641,7 +1609,7 @@ export class AddonModScormProvider { if (success) { // Tracks have been saved, update cached user data. - this.updateUserDataAfterSave(scorm.id, attempt, tracks); + this.updateUserDataAfterSave(scorm.id, attempt, tracks, {cmId: scorm.coursemodule}); this.eventsProvider.trigger(AddonModScormProvider.DATA_SENT_EVENT, { scormId: scorm.id, @@ -1748,10 +1716,11 @@ export class AddonModScormProvider { * @param scormId SCORM ID. * @param attempt Attempt number. * @param tracks Tracking data saved. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when updated. */ - protected updateUserDataAfterSave(scormId: number, attempt: number, tracks: any[], siteId?: string): Promise { + protected updateUserDataAfterSave(scormId: number, attempt: number, tracks: any[], options: {cmId?: number, siteId?: string}) + : Promise { if (!tracks || !tracks.length) { return Promise.resolve(); } @@ -1767,9 +1736,54 @@ export class AddonModScormProvider { } if (needsUpdate) { - return this.getScormUserDataOnline(scormId, attempt, true, siteId); + return this.getScormUserDataOnline(scormId, attempt, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId: options.siteId, + }); } return Promise.resolve(); } } + +/** + * Options to pass to get SCORM. + */ +export type AddonModScormGetScormOptions = CoreSitesCommonWSOptions & { + moduleUrl?: string; // Module URL. +}; + +/** + * Common options with an organization ID. + */ +export type AddonModScormOrganizationOptions = CoreCourseCommonModWSOptions & { + organization?: string; // Organization ID. +}; + +/** + * Common options with offline boolean. + */ +export type AddonModScormOfflineOptions = CoreCourseCommonModWSOptions & { + offline?: boolean; // Whether the attempt is offline. +}; + +/** + * Options to pass to getAttemptCount. + */ +export type AddonModScormGetAttemptCountOptions = CoreCourseCommonModWSOptions & { + ignoreMissing?: boolean; // Whether it should ignore attempts that haven't reported a grade/completion. + userId?: number; // User ID. If not defined use site's current user. +}; + +/** + * Options to pass to getScormUserData. + */ +export type AddonModScormGetUserDataOptions = AddonModScormOfflineOptions & { + scos?: any[]; // SCOs returned by getScos. Recommended if offline=true. +}; + +/** + * Options to pass to getScosWithData. + */ +export type AddonModScormGetScosWithDataOptions = AddonModScormOfflineOptions & AddonModScormOrganizationOptions; diff --git a/src/addon/mod/survey/components/index/addon-mod-survey-index.html b/src/addon/mod/survey/components/index/addon-mod-survey-index.html index eb101c594..9b3531df3 100644 --- a/src/addon/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addon/mod/survey/components/index/addon-mod-survey-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index b7e71e144..4c375e42b 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -143,7 +143,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo * @return Promise resolved when done. */ protected fetchQuestions(): Promise { - return this.surveyProvider.getQuestions(this.survey.id).then((questions) => { + return this.surveyProvider.getQuestions(this.survey.id, {cmId: this.module.id}).then((questions) => { this.questions = this.surveyHelper.formatQuestions(questions); // Init answers object. diff --git a/src/addon/mod/survey/providers/prefetch-handler.ts b/src/addon/mod/survey/providers/prefetch-handler.ts index 026d00c0e..076da7258 100644 --- a/src/addon/mod/survey/providers/prefetch-handler.ts +++ b/src/addon/mod/survey/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -125,7 +125,10 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when done. */ protected prefetchSurvey(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.surveyProvider.getSurvey(courseId, module.id, true, siteId).then((survey) => { + return this.surveyProvider.getSurvey(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((survey) => { const promises = [], files = this.getIntroFilesFromInstance(module, survey); @@ -134,7 +137,11 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan // If survey isn't answered, prefetch the questions. if (!survey.surveydone) { - promises.push(this.surveyProvider.getQuestions(survey.id, true, siteId)); + promises.push(this.surveyProvider.getQuestions(survey.id, { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + })); } return Promise.all(promises); diff --git a/src/addon/mod/survey/providers/survey.ts b/src/addon/mod/survey/providers/survey.ts index 3063c07e8..9e05db3f4 100644 --- a/src/addon/mod/survey/providers/survey.ts +++ b/src/addon/mod/survey/providers/survey.ts @@ -14,14 +14,15 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModSurveyOfflineProvider } from './offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for surveys. @@ -43,24 +44,21 @@ export class AddonModSurveyProvider { * Get a survey's questions. * * @param surveyId Survey ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the questions are retrieved. */ - getQuestions(surveyId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getQuestions(surveyId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - surveyid: surveyId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuestionsCacheKey(surveyId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + surveyid: surveyId, + }; + const preSets = { + cacheKey: this.getQuestionsCacheKey(surveyId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModSurveyProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_survey_get_questions', params, preSets) .then((response: AddonModSurveyGetQuestionsResult): any => { @@ -100,26 +98,22 @@ export class AddonModSurveyProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the survey is retrieved. */ - protected getSurveyDataByKey(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) + protected getSurveyDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSurveyCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getSurveyCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModSurveyProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_survey_get_surveys_by_courses', params, preSets) .then((response: AddonModSurveyGetSurveysByCoursesResult): any => { @@ -143,12 +137,11 @@ export class AddonModSurveyProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the survey is retrieved. */ - getSurvey(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, ignoreCache, siteId); + getSurvey(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, options); } /** @@ -156,12 +149,11 @@ export class AddonModSurveyProvider { * * @param courseId Course ID. * @param id Survey ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the survey is retrieved. */ - getSurveyById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.getSurveyDataByKey(courseId, 'id', id, ignoreCache, siteId); + getSurveyById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getSurveyDataByKey(courseId, 'id', id, options); } /** diff --git a/src/addon/mod/url/providers/url.ts b/src/addon/mod/url/providers/url.ts index 49f2cbe7f..5019cf89b 100644 --- a/src/addon/mod/url/providers/url.ts +++ b/src/addon/mod/url/providers/url.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -107,18 +107,22 @@ export class AddonModUrlProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the url is retrieved. */ - protected getUrlDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getUrlDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getUrlCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getUrlCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModUrlProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_url_get_urls_by_courses', params, preSets) .then((response: AddonModUrlGetUrlsByCoursesResult): any => { @@ -142,11 +146,11 @@ export class AddonModUrlProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the url is retrieved. */ - getUrl(courseId: number, cmId: number, siteId?: string): Promise { - return this.getUrlDataByKey(courseId, 'coursemodule', cmId, siteId); + getUrl(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getUrlDataByKey(courseId, 'coursemodule', cmId, options); } /** diff --git a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html index 44a57c1ca..f9a3da731 100644 --- a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html @@ -19,7 +19,7 @@ - + diff --git a/src/addon/mod/wiki/components/index/index.ts b/src/addon/mod/wiki/components/index/index.ts index ca94dd980..1a52936bb 100644 --- a/src/addon/mod/wiki/components/index/index.ts +++ b/src/addon/mod/wiki/components/index/index.ts @@ -303,7 +303,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp this.pageIsOffline = false; - return this.wikiProvider.getPageContents(pageId); + return this.wikiProvider.getPageContents(pageId, {cmId: this.module.id}); } /** @@ -314,7 +314,11 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp protected fetchSubwikiPages(subwiki: any): Promise { let subwikiPages; - return this.wikiProvider.getSubwikiPages(subwiki.wikiid, subwiki.groupid, subwiki.userid).then((pages) => { + return this.wikiProvider.getSubwikiPages(subwiki.wikiid, { + groupId: subwiki.groupid, + userId: subwiki.userid, + cmId: this.module.id, + }).then((pages) => { subwikiPages = pages; // If no page specified, search first page. @@ -356,7 +360,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp * @param wikiId Wiki ID. */ protected fetchSubwikis(wikiId: number): Promise { - return this.wikiProvider.getSubwikis(wikiId).then((subwikis) => { + return this.wikiProvider.getSubwikis(wikiId, {cmId: this.module.id}).then((subwikis) => { this.loadedSubwikis = subwikis; return this.wikiOffline.subwikisHaveOfflineData(subwikis).then((hasOffline) => { diff --git a/src/addon/mod/wiki/pages/edit/edit.ts b/src/addon/mod/wiki/pages/edit/edit.ts index b0bf66db0..90210ca12 100644 --- a/src/addon/mod/wiki/pages/edit/edit.ts +++ b/src/addon/mod/wiki/pages/edit/edit.ts @@ -155,7 +155,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { this.editOffline = false; // Cannot edit pages in offline. // Get page contents to obtain title and editing permission - promise = this.wikiProvider.getPageContents(this.pageId).then((pageContents) => { + promise = this.wikiProvider.getPageContents(this.pageId, {cmId: this.module.id}).then((pageContents) => { this.pageForm.controls.title.setValue(pageContents.title); // Set the title in the form group. this.wikiId = pageContents.wikiid; this.subwikiId = pageContents.subwikiid; @@ -168,7 +168,11 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { return this.wikiSync.waitForSync(this.blockId); }).then(() => { // Get subwiki files, needed to replace URLs for rich text editor. - return this.wikiProvider.getSubwikiFiles(this.wikiId, this.groupId, this.userId); + return this.wikiProvider.getSubwikiFiles(this.wikiId, { + groupId: this.groupId, + userId: this.userId, + cmId: this.module.id, + }); }).then((files) => { this.subwikiFiles = files; @@ -460,7 +464,13 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { // Try to send the page. let wikiId = this.wikiId || (this.module && this.module.instance); - return this.wikiProvider.newPage(title, text, this.subwikiId, wikiId, this.userId, this.groupId).then((id) => { + return this.wikiProvider.newPage(title, text, { + subwikiId: this.subwikiId, + wikiId, + userId: this.userId, + groupId: this.groupId, + cmId: this.module.id, + }).then((id) => { this.domUtils.triggerFormSubmittedEvent(this.formElement, id > 0, this.sitesProvider.getCurrentSiteId()); @@ -470,7 +480,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { // Page was created, get its data and go to the page. this.pageId = id; - return this.wikiProvider.getPageContents(this.pageId).then((pageContents) => { + return this.wikiProvider.getPageContents(this.pageId, {cmId: this.module.id}).then((pageContents) => { const promises = []; wikiId = parseInt(pageContents.wikiid, 10); diff --git a/src/addon/mod/wiki/providers/create-link-handler.ts b/src/addon/mod/wiki/providers/create-link-handler.ts index f56b8ee8e..f592b7510 100644 --- a/src/addon/mod/wiki/providers/create-link-handler.ts +++ b/src/addon/mod/wiki/providers/create-link-handler.ts @@ -48,13 +48,15 @@ export class AddonModWikiCreateLinkHandler extends CoreContentLinksHandlerBase { protected currentStateIsSameWiki(activeView: ViewController, subwikiId: number, siteId: string): Promise { if (activeView && activeView.component.name == 'AddonModWikiIndexPage') { + const moduleId = activeView.data.module && activeView.data.module.id; + if (activeView.data.subwikiId == subwikiId) { // Same subwiki, so it's same wiki. return Promise.resolve(true); } else if (activeView.data.pageId) { // Get the page contents to check the subwiki. - return this.wikiProvider.getPageContents(activeView.data.pageId, false, false, siteId).then((page) => { + return this.wikiProvider.getPageContents(activeView.data.pageId, {cmId: moduleId, siteId}).then((page) => { return page.subwikiid == subwikiId; }).catch(() => { // Not found, return false. @@ -63,15 +65,14 @@ export class AddonModWikiCreateLinkHandler extends CoreContentLinksHandlerBase { } else if (activeView.data.wikiId) { // Check if the subwiki belongs to this wiki. - return this.wikiProvider.wikiHasSubwiki(activeView.data.wikiId, subwikiId, false, false, siteId); + return this.wikiProvider.wikiHasSubwiki(activeView.data.wikiId, subwikiId, {cmId: moduleId, siteId}); } else if (activeView.data.courseId && activeView.data.module) { - const moduleId = activeView.data.module && activeView.data.module.id; if (moduleId) { // Get the wiki. - return this.wikiProvider.getWiki(activeView.data.courseId, moduleId, false, siteId).then((wiki) => { + return this.wikiProvider.getWiki(activeView.data.courseId, moduleId, {siteId}).then((wiki) => { // Check if the subwiki belongs to this wiki. - return this.wikiProvider.wikiHasSubwiki(wiki.id, subwikiId, false, false, siteId); + return this.wikiProvider.wikiHasSubwiki(wiki.id, subwikiId, {cmId: moduleId, siteId}); }).catch(() => { // Not found, return false. return false; diff --git a/src/addon/mod/wiki/providers/page-or-map-link-handler.ts b/src/addon/mod/wiki/providers/page-or-map-link-handler.ts index cad304c40..d148e4a46 100644 --- a/src/addon/mod/wiki/providers/page-or-map-link-handler.ts +++ b/src/addon/mod/wiki/providers/page-or-map-link-handler.ts @@ -55,7 +55,7 @@ export class AddonModWikiPageOrMapLinkHandler extends CoreContentLinksHandlerBas action = url.indexOf('mod/wiki/map.php') != -1 ? 'map' : 'page'; // Get the page data to obtain wikiId, subwikiId, etc. - this.wikiProvider.getPageContents(pageId, false, false, siteId).then((page) => { + this.wikiProvider.getPageContents(pageId, {siteId}).then((page) => { let promise; if (courseId) { promise = Promise.resolve(courseId); diff --git a/src/addon/mod/wiki/providers/prefetch-handler.ts b/src/addon/mod/wiki/providers/prefetch-handler.ts index f9677e893..af4c3e15b 100644 --- a/src/addon/mod/wiki/providers/prefetch-handler.ts +++ b/src/addon/mod/wiki/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -65,18 +65,15 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl * * @param module The module object returned by WS. * @param courseId The course ID. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return List of pages. */ - protected getAllPages(module: any, courseId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + protected getAllPages(module: any, courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.wikiProvider.getWiki(courseId, module.id, offline, siteId).then((wiki) => { - return this.wikiProvider.getWikiPageList(wiki, offline, ignoreCache, siteId); + return this.wikiProvider.getWiki(courseId, module.id, options).then((wiki) => { + return this.wikiProvider.getWikiPageList(wiki, options); }).catch(() => { // Wiki not found, return empty list. return []; @@ -100,7 +97,10 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl return this.pluginFileDelegate.getFilesDownloadSize(files); })); - promises.push(this.getAllPages(module, courseId, false, true, siteId).then((pages) => { + promises.push(this.getAllPages(module, courseId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((pages) => { let size = 0; pages.forEach((page) => { @@ -133,10 +133,10 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.wikiProvider.getWiki(courseId, module.id, false, siteId).then((wiki) => { + return this.wikiProvider.getWiki(courseId, module.id, {siteId}).then((wiki) => { const introFiles = this.getIntroFilesFromInstance(module, wiki); - return this.wikiProvider.getWikiFileList(wiki, false, false, siteId).then((files) => { + return this.wikiProvider.getWikiFileList(wiki, {siteId}).then((files) => { return introFiles.concat(files); }); }).catch(() => { @@ -191,14 +191,23 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl protected prefetchWiki(module: any, courseId: number, single: boolean, siteId: string, downloadTime: number): Promise { const userId = this.sitesProvider.getCurrentSiteUserId(); + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + // Get the list of pages. - return this.getAllPages(module, courseId, false, true, siteId).then((pages) => { + return this.getAllPages(module, courseId, commonOptions).then((pages) => { const promises = []; pages.forEach((page) => { // Fetch page contents if it needs to be fetched. if (page.timemodified > downloadTime) { - promises.push(this.wikiProvider.getPageContents(page.id, false, true, siteId)); + promises.push(this.wikiProvider.getPageContents(page.id, modOptions)); } }); @@ -206,7 +215,7 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl promises.push(this.groupsProvider.getActivityGroupInfo(module.id, false, userId, siteId)); // Fetch info to provide wiki links. - promises.push(this.wikiProvider.getWiki(courseId, module.id, false, siteId).then((wiki) => { + promises.push(this.wikiProvider.getWiki(courseId, module.id, {siteId}).then((wiki) => { return this.courseHelper.getModuleCourseIdByInstance(wiki.id, 'wiki', siteId); })); diff --git a/src/addon/mod/wiki/providers/wiki-sync.ts b/src/addon/mod/wiki/providers/wiki-sync.ts index 174fedcf6..4309c741c 100644 --- a/src/addon/mod/wiki/providers/wiki-sync.ts +++ b/src/addon/mod/wiki/providers/wiki-sync.ts @@ -266,8 +266,13 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { // Send the pages. pages.forEach((page) => { - promises.push(this.wikiProvider.newPageOnline(page.title, page.cachedcontent, subwikiId, wikiId, userId, groupId, - siteId).then((pageId) => { + promises.push(this.wikiProvider.newPageOnline(page.title, page.cachedcontent, { + subwikiId, + wikiId, + userId, + groupId, + siteId, + }).then((pageId) => { result.updated = true; @@ -339,7 +344,7 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { // Ignore errors. }).then(() => { // Sync is done at subwiki level, get all the subwikis. - return this.wikiProvider.getSubwikis(wikiId); + return this.wikiProvider.getSubwikis(wikiId, {cmId}); }).then((subwikis) => { const promises = [], result: AddonModWikiSyncWikiResult = { diff --git a/src/addon/mod/wiki/providers/wiki.ts b/src/addon/mod/wiki/providers/wiki.ts index ee9414c54..76c000570 100644 --- a/src/addon/mod/wiki/providers/wiki.ts +++ b/src/addon/mod/wiki/providers/wiki.ts @@ -16,13 +16,14 @@ 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 { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModWikiOfflineProvider } from './wiki-offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; export interface AddonModWikiSubwikiListData { /** @@ -118,27 +119,21 @@ export class AddonModWikiProvider { * Get a wiki page contents. * * @param pageId Page ID. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the page data. */ - getPageContents(pageId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getPageContents(pageId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - pageid: pageId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getPageContentsCacheKey(pageId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + pageid: pageId, + }; + const preSets = { + cacheKey: this.getPageContentsCacheKey(pageId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_page_contents', params, preSets).then((response) => { return response.page || Promise.reject(null); @@ -191,36 +186,27 @@ export class AddonModWikiProvider { * Gets the list of files from a specific subwiki. * * @param wikiId Wiki ID. - * @param groupId Group to get files from. - * @param userId User to get files from. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with subwiki files. */ - getSubwikiFiles(wikiId: number, groupId?: number, userId?: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getSubwikiFiles(wikiId: number, options: AddonModWikiGetSubwikiFilesOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - groupId = groupId || -1; - userId = userId || 0; + return this.sitesProvider.getSite(options.siteId).then((site) => { + const groupId = options.groupId || -1; + const userId = options.userId || 0; const params = { - wikiid: wikiId, - groupid: groupId, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubwikiFilesCacheKey(wikiId, groupId, userId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + wikiid: wikiId, + groupid: groupId, + userid: userId, + }; + const preSets = { + cacheKey: this.getSubwikiFilesCacheKey(wikiId, groupId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_subwiki_files', params, preSets).then((response) => { return response.files || Promise.reject(null); @@ -264,48 +250,34 @@ export class AddonModWikiProvider { * Get the list of Pages of a SubWiki. * * @param wikiId Wiki ID. - * @param groupId Group to get pages from. - * @param userId User to get pages from. - * @param sortBy The attribute to sort the returned list. - * @param sortDirection Direction to sort the returned list (ASC | DESC). - * @param includeContent Whether the pages have to include its content. Default: false. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with wiki subwiki pages. */ - getSubwikiPages(wikiId: number, groupId?: number, userId?: number, sortBy: string = 'title', sortDirection: string = 'ASC', - includeContent?: boolean, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + getSubwikiPages(wikiId: number, options: AddonModWikiGetSubwikiPagesOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - groupId = groupId || -1; - userId = userId || 0; - sortBy = sortBy || 'title'; - sortDirection = sortDirection || 'ASC'; - includeContent = includeContent || false; + return this.sitesProvider.getSite(options.siteId).then((site) => { + const groupId = options.groupId || -1; + const userId = options.userId || 0; + const sortBy = options.sortBy || 'title'; + const sortDirection = options.sortDirection || 'ASC'; const params = { - wikiid: wikiId, - groupid: groupId, - userid: userId, - options: { - sortby: sortBy, - sortdirection: sortDirection, - includecontent: includeContent ? 1 : 0 - } - + wikiid: wikiId, + groupid: groupId, + userid: userId, + options: { + sortby: sortBy, + sortdirection: sortDirection, + includecontent: options.includeContent ? 1 : 0, }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubwikiPagesCacheKey(wikiId, groupId, userId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + }; + const preSets = { + cacheKey: this.getSubwikiPagesCacheKey(wikiId, groupId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_subwiki_pages', params, preSets).then((response) => { return response.pages || Promise.reject(null); @@ -339,27 +311,21 @@ export class AddonModWikiProvider { * Get all the subwikis of a wiki. * * @param wikiId Wiki ID. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with subwikis. */ - getSubwikis(wikiId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getSubwikis(wikiId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - wikiid: wikiId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubwikisCacheKey(wikiId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + wikiid: wikiId, + }; + const preSets = { + cacheKey: this.getSubwikisCacheKey(wikiId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_subwikis', params, preSets).then((response) => { return response.subwikis || Promise.reject(null); @@ -382,12 +348,11 @@ export class AddonModWikiProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the wiki is retrieved. */ - getWiki(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { - return this.getWikiByField(courseId, 'coursemodule', cmId, forceCache, siteId); + getWiki(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWikiByField(courseId, 'coursemodule', cmId, options); } /** @@ -396,20 +361,21 @@ export class AddonModWikiProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the wiki is retrieved. */ - protected getWikiByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise { + protected getWikiByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getWikiDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getWikiDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWikiProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_wikis_by_courses', params, preSets).then((response) => { if (response.wikis) { @@ -432,12 +398,11 @@ export class AddonModWikiProvider { * * @param courseId Course ID. * @param id Wiki ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the wiki is retrieved. */ - getWikiById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { - return this.getWikiByField(courseId, 'id', id, forceCache, siteId); + getWikiById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWikiByField(courseId, 'id', id, options); } /** @@ -454,22 +419,29 @@ export class AddonModWikiProvider { * Gets a list of files to download for a wiki, using a format similar to module.contents from get_course_contents. * * @param wiki Wiki. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of files. */ - getWikiFileList(wiki: any, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getWikiFileList(wiki: any, options: CoreSitesCommonWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); let files = []; + const modOptions = { + cmId: wiki.coursemodule, + ...options, // Include all options. + }; - return this.getSubwikis(wiki.id, offline, ignoreCache, siteId).then((subwikis) => { + return this.getSubwikis(wiki.id, modOptions).then((subwikis) => { const promises = []; subwikis.forEach((subwiki) => { - promises.push(this.getSubwikiFiles(subwiki.wikiid, subwiki.groupid, subwiki.userid, offline, ignoreCache, siteId) - .then((swFiles) => { + const subwikiOptions = { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...modOptions, // Include all options. + }; + + promises.push(this.getSubwikiFiles(subwiki.wikiid, subwikiOptions).then((swFiles) => { files = files.concat(swFiles); })); }); @@ -484,22 +456,27 @@ export class AddonModWikiProvider { * Gets a list of all pages for a Wiki. * * @param wiki Wiki. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Page list. */ - getWikiPageList(wiki: any, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getWikiPageList(wiki: any, options: CoreSitesCommonWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); let pages = []; + const modOptions = { + cmId: wiki.coursemodule, + ...options, // Include all options. + }; - return this.getSubwikis(wiki.id, offline, ignoreCache, siteId).then((subwikis) => { + return this.getSubwikis(wiki.id, modOptions).then((subwikis) => { const promises = []; subwikis.forEach((subwiki) => { - promises.push(this.getSubwikiPages(subwiki.wikiid, subwiki.groupid, subwiki.userid, undefined, undefined, - undefined, offline, ignoreCache, siteId).then((subwikiPages) => { + promises.push(this.getSubwikiPages(subwiki.wikiid, { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...modOptions, // Include all options. + }).then((subwikiPages) => { pages = pages.concat(subwikiPages); })); }); @@ -522,7 +499,7 @@ export class AddonModWikiProvider { invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.getWiki(courseId, moduleId, false, siteId).then((wiki) => { + return this.getWiki(courseId, moduleId, {siteId}).then((wiki) => { const promises = []; promises.push(this.invalidateWikiData(courseId, siteId)); @@ -618,16 +595,13 @@ export class AddonModWikiProvider { * @param wikiId Wiki ID. * @param subwikiId Subwiki ID. * @param title Page title. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with true if used, resolved with false if not used or cannot determine. */ - isTitleUsed(wikiId: number, subwikiId: number, title: string, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + isTitleUsed(wikiId: number, subwikiId: number, title: string, options: CoreCourseCommonModWSOptions = {}): Promise { // First get the subwiki. - return this.getSubwikis(wikiId, offline, ignoreCache, siteId).then((subwikis) => { + return this.getSubwikis(wikiId, options).then((subwikis) => { // Search the subwiki. const subwiki = subwikis.find((subwiki) => { return subwiki.id == subwikiId; @@ -636,8 +610,11 @@ export class AddonModWikiProvider { return subwiki || Promise.reject(null); }).then((subwiki) => { // Now get all the pages of the subwiki. - return this.getSubwikiPages(wikiId, subwiki.groupid, subwiki.userid, undefined, undefined, false, offline, - ignoreCache, siteId); + return this.getSubwikiPages(wikiId, { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...options, // Include all options. + }); }).then((pages) => { // Check if there's any page with the same title. const page = pages.find((page) => { @@ -690,25 +667,24 @@ export class AddonModWikiProvider { * * @param title Title to create the page. * @param content Content to save on the page. - * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. - * @param wikiId Wiki ID. Optional, will be used to create a new subwiki if subwikiId not supplied. - * @param userId User ID. Optional, will be used to create a new subwiki if subwikiId not supplied. - * @param groupId Group ID. Optional, will be used to create a new subwiki if subwikiId not supplied. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with page ID if page was created in server, -1 if stored in device. */ - newPage(title: string, content: string, subwikiId?: number, wikiId?: number, userId?: number, groupId?: number, - siteId?: string): Promise { + newPage(title: string, content: string, options: AddonModWikiNewPageOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a new page to be synchronized later. const storeOffline = (): Promise => { let promise; - if (wikiId) { + if (options.wikiId) { // We have wiki ID, check if there's already an online page with this title and subwiki. - promise = this.isTitleUsed(wikiId, subwikiId, title, true, false, siteId).catch(() => { + promise = this.isTitleUsed(options.wikiId, options.subwikiId, title, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).catch(() => { // Error, assume not used. return false; }).then((used) => { @@ -721,7 +697,8 @@ export class AddonModWikiProvider { } return promise.then(() => { - return this.wikiOffline.saveNewPage(title, content, subwikiId, wikiId, userId, groupId, siteId).then(() => { + return this.wikiOffline.saveNewPage(title, content, options.subwikiId, options.wikiId, options.userId, + options.groupId, options.siteId).then(() => { return -1; }); }); @@ -733,9 +710,10 @@ export class AddonModWikiProvider { } // Discard stored content for this page. If it exists it means the user is editing it. - return this.wikiOffline.deleteNewPage(title, subwikiId, wikiId, userId, groupId, siteId).then(() => { + return this.wikiOffline.deleteNewPage(title, options.subwikiId, options.wikiId, options.userId, options.groupId, + options.siteId).then(() => { // Try to create it in online. - return this.newPageOnline(title, content, subwikiId, wikiId, userId, groupId, siteId).catch((error) => { + return this.newPageOnline(title, content, options).catch((error) => { if (this.utils.isWebServiceError(error)) { // The WebService has thrown an error, this means that the page cannot be added. return Promise.reject(error); @@ -752,32 +730,27 @@ export class AddonModWikiProvider { * * @param title Title to create the page. * @param content Content to save on the page. - * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. - * @param wikiId Wiki ID. Optional, will be used create subwiki if not informed. - * @param userId User ID. Optional, will be used create subwiki if not informed. - * @param groupId Group ID. Optional, will be used create subwiki if not informed. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the page ID if created, rejected otherwise. */ - newPageOnline(title: string, content: string, subwikiId?: number, wikiId?: number, userId?: number, groupId?: number, - siteId?: string): Promise { + newPageOnline(title: string, content: string, options: AddonModWikiNewPageOnlineOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - title: title, - content: content, - contentformat: 'html' - }; + title: title, + content: content, + contentformat: 'html', + }; - subwikiId = this.wikiOffline.convertToPositiveNumber(subwikiId); - wikiId = this.wikiOffline.convertToPositiveNumber(wikiId); + const subwikiId = this.wikiOffline.convertToPositiveNumber(options.subwikiId); + const wikiId = this.wikiOffline.convertToPositiveNumber(options.wikiId); if (subwikiId && subwikiId > 0) { params.subwikiid = subwikiId; } else if (wikiId) { params.wikiid = wikiId; - params.userid = this.wikiOffline.convertToPositiveNumber(userId); - params.groupid = this.wikiOffline.convertToPositiveNumber(groupId); + params.userid = this.wikiOffline.convertToPositiveNumber(options.userId); + params.groupid = this.wikiOffline.convertToPositiveNumber(options.groupId); } return site.write('mod_wiki_new_page', params).then((response) => { @@ -830,14 +803,12 @@ export class AddonModWikiProvider { * * @param wikiId Wiki ID. * @param subwikiId Subwiki ID to search. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with true if it has subwiki, resolved with false otherwise. */ - wikiHasSubwiki(wikiId: number, subwikiId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + wikiHasSubwiki(wikiId: number, subwikiId: number, options: CoreCourseCommonModWSOptions = {}): Promise { // Get the subwikis to check if any of them matches the one passed as param. - return this.getSubwikis(wikiId, offline, ignoreCache, siteId).then((subwikis) => { + return this.getSubwikis(wikiId, options).then((subwikis) => { const subwiki = subwikis.find((subwiki) => { return subwiki.id == subwikiId; }); @@ -849,3 +820,40 @@ export class AddonModWikiProvider { }); } } + +/** + * Options to pass to getSubwikiFiles. + */ +export type AddonModWikiGetSubwikiFilesOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User to get files from. + groupId?: number; // Group to get files from. +}; + +/** + * Options to pass to getSubwikiPages. + */ +export type AddonModWikiGetSubwikiPagesOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User to get pages from. + groupId?: number; // Group to get pages from. + sortBy?: string; // The attribute to sort the returned list. Defaults to 'title'. + sortDirection?: string; // Direction to sort the returned list (ASC | DESC). Defaults to 'ASC'. + includeContent?: boolean; // Whether the pages have to include their content. +}; + +/** + * Options to pass to newPageOnline. + */ +export type AddonModWikiNewPageOnlineOptions = { + subwikiId?: number; // Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + wikiId?: number; // Wiki ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + userId?: number; // User ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + groupId?: number; // Group ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to newPage. + */ +export type AddonModWikiNewPageOptions = AddonModWikiNewPageOnlineOptions & { + cmId?: number; // Module ID. +}; diff --git a/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html b/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html index c3bb1fd41..1f71f1a4a 100644 --- a/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html +++ b/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html @@ -23,7 +23,7 @@ [maxSubmissions]="workshop.overallfeedbackfiles" [component]="component" [componentId]="componentId" [allowOffline]="true"> {{ 'addon.mod_workshop.assessmentweight' | translate }} - + {{w}} diff --git a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts index 0eda6bceb..7418e92b5 100644 --- a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts +++ b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts @@ -147,8 +147,10 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit { * @return Promised resvoled when data is loaded. */ protected load(): Promise { - return this.workshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, this.userId) - .then((assessmentData) => { + return this.workshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, { + userId: this.userId, + cmId: this.workshop.coursemodule, + }).then((assessmentData) => { this.data.assessment = assessmentData; let promise; diff --git a/src/addon/mod/workshop/components/assessment/addon-mod-workshop-assessment.html b/src/addon/mod/workshop/components/assessment/addon-mod-workshop-assessment.html index 8c876cfd1..897088a8e 100644 --- a/src/addon/mod/workshop/components/assessment/addon-mod-workshop-assessment.html +++ b/src/addon/mod/workshop/components/assessment/addon-mod-workshop-assessment.html @@ -1,5 +1,5 @@ -
+

{{profile.fullname}}

@@ -16,10 +16,10 @@ {{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }}

{{ 'addon.mod_workshop.notassessed' | translate }} - - + + {{ 'core.notsent' | translate }} -
+ diff --git a/src/addon/mod/workshop/components/assessment/assessment.ts b/src/addon/mod/workshop/components/assessment/assessment.ts index a565bf5a6..b7a13569f 100644 --- a/src/addon/mod/workshop/components/assessment/assessment.ts +++ b/src/addon/mod/workshop/components/assessment/assessment.ts @@ -99,8 +99,11 @@ export class AddonModWorkshopAssessmentComponent implements OnInit { /** * Navigate to the assessment. */ - gotoAssessment(): void { + gotoAssessment(event: Event): void { if (!this.canSelfAssess && this.canViewAssessment) { + event.preventDefault(); + event.stopPropagation(); + const params = { assessment: this.assessment, submission: this.submission, @@ -110,10 +113,10 @@ export class AddonModWorkshopAssessmentComponent implements OnInit { }; if (!this.submission) { - const modal = this.domUtils.showModalLoading('core.sending', true); + const modal = this.domUtils.showModalLoading(); - this.workshopHelper.getSubmissionById(this.workshop.id, this.assessment.submissionid) - .then((submissionData) => { + this.workshopHelper.getSubmissionById(this.workshop.id, this.assessment.submissionid, + {cmId: this.workshop.coursemodule}).then((submissionData) => { params.submission = submissionData; this.navCtrl.push('AddonModWorkshopAssessmentPage', params); @@ -131,8 +134,11 @@ export class AddonModWorkshopAssessmentComponent implements OnInit { /** * Navigate to my own assessment. */ - gotoOwnAssessment(): void { + gotoOwnAssessment(event: Event): void { if (this.canSelfAssess) { + event.preventDefault(); + event.stopPropagation(); + const params = { module: this.module, workshop: this.workshop, diff --git a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html index f662c02ae..7e8daa461 100644 --- a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html +++ b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index d14b2b04e..d2e7b7903 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -198,7 +198,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity } }).then(() => { // Check if there are answers stored in offline. - return this.workshopProvider.getWorkshopAccessInformation(this.workshop.id); + return this.workshopProvider.getWorkshopAccessInformation(this.workshop.id, {cmId: this.module.id}); }).then((accessData) => { this.access = accessData; @@ -209,7 +209,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity }); } }).then(() => { - return this.workshopProvider.getUserPlanPhases(this.workshop.id); + return this.workshopProvider.getUserPlanPhases(this.workshop.id, {cmId: this.module.id}); }).then((phases) => { this.phases = phases; @@ -245,7 +245,11 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity * @return Resolved when done. */ gotoSubmissionsPage(page: number): Promise { - return this.workshopProvider.getGradesReport(this.workshop.id, this.group, page).then((report) => { + return this.workshopProvider.getGradesReport(this.workshop.id, { + groupId: this.group, + page, + cmId: this.module.id, + }).then((report) => { const numEntries = (report && report.grades && report.grades.length) || 0; this.page = page; @@ -348,7 +352,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity const promises = []; if (this.canSubmit) { - promises.push(this.workshopHelper.getUserSubmission(this.workshop.id).then((submission) => { + promises.push(this.workshopHelper.getUserSubmission(this.workshop.id, {cmId: this.module.id}).then((submission) => { const actions = this.workshopHelper.filterSubmissionActions(this.offlineSubmissions, submission.id || false); return this.workshopHelper.applyOfflineData(submission, actions).then((submission) => { @@ -366,7 +370,9 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity if (this.workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT) { this.canAssess = this.workshopHelper.canAssess(this.workshop, this.access); if (this.canAssess) { - assessPromise = this.workshopHelper.getReviewerAssessments(this.workshop.id).then((assessments) => { + assessPromise = this.workshopHelper.getReviewerAssessments(this.workshop.id, { + cmId: this.module.id, + }).then((assessments) => { const p2 = []; assessments.forEach((assessment) => { @@ -391,13 +397,13 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity } if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { - promises.push(this.workshopProvider.getGrades(this.workshop.id).then((grades) => { + promises.push(this.workshopProvider.getGrades(this.workshop.id, {cmId: this.module.id}).then((grades) => { this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : false; })); if (this.access.canviewpublishedsubmissions) { promises.push(assessPromise.then(() => { - return this.workshopProvider.getSubmissions(this.workshop.id).then((submissions) => { + return this.workshopProvider.getSubmissions(this.workshop.id, {cmId: this.module.id}).then((submissions) => { this.publishedSubmissions = submissions.filter((submission) => { if (submission.published) { this.assessments.forEach((assessment) => { diff --git a/src/addon/mod/workshop/pages/assessment/assessment.ts b/src/addon/mod/workshop/pages/assessment/assessment.ts index 5cc584349..337bd1a63 100644 --- a/src/addon/mod/workshop/pages/assessment/assessment.ts +++ b/src/addon/mod/workshop/pages/assessment/assessment.ts @@ -150,7 +150,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { }).then((gradeInfo) => { this.maxGrade = gradeInfo.grade; - return this.workshopProvider.getWorkshopAccessInformation(this.workshopId); + return this.workshopProvider.getWorkshopAccessInformation(this.workshopId, {cmId: this.workshop.coursemodule}); }).then((accessData) => { this.access = accessData; @@ -168,8 +168,10 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { if (this.evaluating || this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { // Get all info of the assessment. - return this.workshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, - this.profile && this.profile.id).then((assessment) => { + return this.workshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, { + userId: this.profile && this.profile.id, + cmId: this.workshop.coursemodule, + }).then((assessment) => { let defaultGrade, promise; this.assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts index 6d4f6d704..ea8e03f45 100644 --- a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts @@ -149,7 +149,8 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { if (this.submissionId > 0) { this.editing = true; - return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId).then((submissionData) => { + return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId, {cmId: this.module.id}) + .then((submissionData) => { this.submission = submissionData; const canEdit = (this.userId == submissionData.authorid && this.access.cansubmit && diff --git a/src/addon/mod/workshop/pages/submission/submission.ts b/src/addon/mod/workshop/pages/submission/submission.ts index 245d320ac..f1d0ec97b 100644 --- a/src/addon/mod/workshop/pages/submission/submission.ts +++ b/src/addon/mod/workshop/pages/submission/submission.ts @@ -190,7 +190,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { * @return Resolved when done. */ protected fetchSubmissionData(): Promise { - return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId).then((submissionData) => { + return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId, { + cmId: this.module.id, + }).then((submissionData) => { const promises = []; this.submission = submissionData; @@ -207,8 +209,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { if (this.access.canviewallassessments) { // Get new data, different that came from stateParams. - promises.push(this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId) - .then((subAssessments) => { + promises.push(this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId, { + cmId: this.module.id, + }).then((subAssessments) => { // Only allow the student to delete their own submission if it's still editable and hasn't been assessed. if (this.canDelete) { this.canDelete = !subAssessments.length; @@ -228,7 +231,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { })); } else if (this.currentUserId == this.userId && this.assessmentId) { // Get new data, different that came from stateParams. - promises.push(this.workshopProvider.getAssessment(this.workshopId, this.assessmentId).then((assessment) => { + promises.push(this.workshopProvider.getAssessment(this.workshopId, this.assessmentId, { + cmId: this.module.id, + }).then((assessment) => { // Only allow the student to delete their own submission if it's still editable and hasn't been assessed. if (this.canDelete) { this.canDelete = !assessment; @@ -239,7 +244,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { this.submissionInfo.reviewedby = [assessment]; })); } else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && this.userId == this.currentUserId) { - this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId).then((assessments) => { + this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId, { + cmId: this.module.id, + }).then((assessments) => { this.submissionInfo.reviewedby = assessments.map((assessment) => { return this.parseAssessment(assessment); }); diff --git a/src/addon/mod/workshop/providers/helper.ts b/src/addon/mod/workshop/providers/helper.ts index ad82e99e9..5ef4d8f8f 100644 --- a/src/addon/mod/workshop/providers/helper.ts +++ b/src/addon/mod/workshop/providers/helper.ts @@ -19,7 +19,7 @@ import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploa import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { AddonModWorkshopProvider } from './workshop'; +import { AddonModWorkshopProvider, AddonModWorkshopUserOptions } from './workshop'; import { AddonModWorkshopOfflineProvider } from './offline'; import { AddonWorkshopAssessmentStrategyDelegate } from './assessment-strategy-delegate'; @@ -109,12 +109,13 @@ export class AddonModWorkshopHelperProvider { * Return a particular user submission from the submission list. * * @param workshopId Workshop ID. - * @param userId User ID. If not defined current user Id. + * @param options Other options. * @return Resolved with the submission, resolved with false if not found. */ - getUserSubmission(workshopId: number, userId: number = 0): Promise { - return this.workshopProvider.getSubmissions(workshopId).then((submissions) => { - userId = userId || this.sitesProvider.getCurrentSiteUserId(); + getUserSubmission(workshopId: number, options: AddonModWorkshopUserOptions = {}): Promise { + const userId = options.userId || this.sitesProvider.getCurrentSiteUserId(); + + return this.workshopProvider.getSubmissions(workshopId, options).then((submissions) => { for (const x in submissions) { if (submissions[x].authorid == userId) { @@ -131,13 +132,12 @@ export class AddonModWorkshopHelperProvider { * * @param workshopId Workshop ID. * @param submissionId Submission ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the submission, resolved with false if not found. */ - getSubmissionById(workshopId: number, submissionId: number, siteId?: string): Promise { - return this.workshopProvider.getSubmission(workshopId, submissionId, siteId).catch(() => { - return this.workshopProvider.getSubmissions(workshopId, undefined, undefined, undefined, undefined, siteId) - .then((submissions) => { + getSubmissionById(workshopId: number, submissionId: number, options: {cmId?: number, siteId?: string} = {}): Promise { + return this.workshopProvider.getSubmission(workshopId, submissionId, options).catch(() => { + return this.workshopProvider.getSubmissions(workshopId, options).then((submissions) => { for (const x in submissions) { if (submissions[x].id == submissionId) { return submissions[x]; @@ -154,14 +154,12 @@ export class AddonModWorkshopHelperProvider { * * @param workshopId Workshop ID. * @param assessmentId Assessment ID. - * @param userId User ID. If not defined, current user. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the assessment. */ - getReviewerAssessmentById(workshopId: number, assessmentId: number, userId: number = 0, siteId?: string): Promise { - return this.workshopProvider.getAssessment(workshopId, assessmentId, siteId).catch((error) => { - return this.workshopProvider.getReviewerAssessments(workshopId, userId, undefined, undefined, siteId) - .then((assessments) => { + getReviewerAssessmentById(workshopId: number, assessmentId: number, options: AddonModWorkshopUserOptions = {}): Promise { + return this.workshopProvider.getAssessment(workshopId, assessmentId, options).catch((error) => { + return this.workshopProvider.getReviewerAssessments(workshopId, options).then((assessments) => { for (const x in assessments) { if (assessments[x].id == assessmentId) { return assessments[x]; @@ -172,8 +170,7 @@ export class AddonModWorkshopHelperProvider { return Promise.reject(error); }); }).then((assessment) => { - return this.workshopProvider.getAssessmentForm(workshopId, assessmentId, undefined, undefined, undefined, siteId) - .then((assessmentForm) => { + return this.workshopProvider.getAssessmentForm(workshopId, assessmentId, options).then((assessmentForm) => { assessment.form = assessmentForm; return assessment; @@ -185,22 +182,21 @@ export class AddonModWorkshopHelperProvider { * Retrieves the assessment of the given user and all the related data. * * @param workshopId Workshop ID. - * @param userId User ID. If not defined, current user. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getReviewerAssessments(workshopId: number, userId: number = 0, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getReviewerAssessments(workshopId: number, options: AddonModWorkshopUserOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.workshopProvider.getReviewerAssessments(workshopId, userId, offline, ignoreCache, siteId) - .then((assessments) => { - const promises = assessments.map((assessment) => { - return this.getSubmissionById(workshopId, assessment.submissionid, siteId).then((submission) => { + return this.workshopProvider.getReviewerAssessments(workshopId, options).then((assessments) => { + const promises = []; + assessments.forEach((assessment) => { + promises.push(this.getSubmissionById(workshopId, assessment.submissionid, options).then((submission) => { assessment.submission = submission; - }); + })); + promises.push(this.workshopProvider.getAssessmentForm(workshopId, assessment.id, options).then((assessmentForm) => { + assessment.form = assessmentForm; + })); }); return Promise.all(promises).then(() => { diff --git a/src/addon/mod/workshop/providers/prefetch-handler.ts b/src/addon/mod/workshop/providers/prefetch-handler.ts index 1213804c6..c189a9f60 100644 --- a/src/addon/mod/workshop/providers/prefetch-handler.ts +++ b/src/addon/mod/workshop/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -68,7 +68,7 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * @return Promise resolved with the list of files. */ getFiles(module: any, courseId: number, single?: boolean): Promise { - return this.getWorkshopInfoHelper(module, courseId, true).then((info) => { + return this.getWorkshopInfoHelper(module, courseId, {omitFail: true}).then((info) => { return info.files; }); } @@ -78,31 +78,32 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * * @param module Module to get the files. * @param courseId Course ID the module belongs to. - * @param omitFail True to always return even if fails. Default false. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the info fetched. */ - protected getWorkshopInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - let workshop, - groups = [], - files = [], - access; + protected getWorkshopInfoHelper(module: any, courseId: number, options: AddonModWorkshopGetInfoOptions = {}): Promise { + let workshop; + let groups = []; + let files = []; + let access; + const modOptions = { + cmId: module.id, + ...options, // Include all options. + }; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const userId = site.getUserId(); - return this.workshopProvider.getWorkshop(courseId, module.id, siteId, forceCache).then((data) => { + return this.workshopProvider.getWorkshop(courseId, module.id, options).then((data) => { files = this.getIntroFilesFromInstance(module, data); files = files.concat(data.instructauthorsfiles).concat(data.instructreviewersfiles); workshop = data; - return this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId).then((accessData) => { + return this.workshopProvider.getWorkshopAccessInformation(workshop.id, modOptions).then((accessData) => { access = accessData; if (access.canviewallsubmissions) { - return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => { + return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, options.siteId) + .then((groupInfo) => { if (!groupInfo.groups || groupInfo.groups.length == 0) { groupInfo.groups = [{id: 0}]; } @@ -111,7 +112,7 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH } }); }).then(() => { - return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => { + return this.workshopProvider.getUserPlanPhases(workshop.id, modOptions).then((phases) => { // Get submission phase info. const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION], canSubmit = this.workshopHelper.canSubmit(workshop, access, submissionPhase.tasks), @@ -119,7 +120,10 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH promises = []; if (canSubmit) { - promises.push(this.workshopHelper.getUserSubmission(workshop.id, userId).then((submission) => { + promises.push(this.workshopHelper.getUserSubmission(workshop.id, { + userId, + cmId: module.id, + }).then((submission) => { if (submission) { files = files.concat(submission.contentfiles).concat(submission.attachmentfiles); } @@ -127,16 +131,22 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH } if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { - promises.push(this.workshopProvider.getSubmissions(workshop.id).then((submissions) => { + promises.push(this.workshopProvider.getSubmissions(workshop.id, modOptions).then((submissions) => { const promises2 = []; submissions.forEach((submission) => { files = files.concat(submission.contentfiles).concat(submission.attachmentfiles); - promises2.push(this.workshopProvider.getSubmissionAssessments(workshop.id, submission.id) - .then((assessments) => { + promises2.push(this.workshopProvider.getSubmissionAssessments(workshop.id, submission.id, { + cmId: module.id, + }).then((assessments) => { assessments.forEach((assessment) => { files = files.concat(assessment.feedbackattachmentfiles) .concat(assessment.feedbackcontentfiles); }); + if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { + return Promise.all(assessments.map((assessment) => { + return this.workshopHelper.getReviewerAssessmentById(workshop.id, assessment.id); + })); + } })); }); @@ -146,7 +156,7 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH // Get assessment files. if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { - promises.push(this.workshopHelper.getReviewerAssessments(workshop.id).then((assessments) => { + promises.push(this.workshopHelper.getReviewerAssessments(workshop.id, modOptions).then((assessments) => { assessments.forEach((assessment) => { files = files.concat(assessment.feedbackattachmentfiles).concat(assessment.feedbackcontentfiles); }); @@ -163,7 +173,7 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH files: files.filter((file) => typeof file !== 'undefined') }; }).catch((message): any => { - if (omitFail) { + if (options.omitFail) { // Any error, return the info we have. return { workshop: workshop, @@ -195,8 +205,10 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * @return Whether the module can be downloaded. The promise should never be rejected. */ isDownloadable(module: any, courseId: number): boolean | Promise { - return this.workshopProvider.getWorkshop(courseId, module.id, undefined, true).then((workshop) => { - return this.workshopProvider.getWorkshopAccessInformation(workshop.id).then((accessData) => { + return this.workshopProvider.getWorkshop(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((workshop) => { + return this.workshopProvider.getWorkshopAccessInformation(workshop.id, {cmId: module.id}).then((accessData) => { // Check if workshop is setup by phase. return accessData.canswitchphase || workshop.phase > AddonModWorkshopProvider.PHASE_SETUP; }); @@ -230,15 +242,15 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * * @param workshopId Workshop ID. * @param groups Array of groups in the activity. + * @param cmId Module ID. * @param siteId Site ID. If not defined, current site. * @return All unique entries. */ - protected getAllGradesReport(workshopId: number, groups: any[], siteId: string): Promise { + protected getAllGradesReport(workshopId: number, groups: any[], cmId: number, siteId: string): Promise { const promises = []; groups.forEach((group) => { - promises.push(this.workshopProvider.fetchAllGradeReports( - workshopId, group.id, undefined, false, false, siteId)); + promises.push(this.workshopProvider.fetchAllGradeReports(workshopId, {groupId: group.id, cmId, siteId})); }); return Promise.all(promises).then((grades) => { @@ -266,23 +278,31 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * @return Promise resolved when done. */ protected prefetchWorkshop(module: any, courseId: number, single: boolean, siteId: string): Promise { - const userIds = []; siteId = siteId || this.sitesProvider.getCurrentSiteId(); + const userIds = []; + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + return this.sitesProvider.getSite(siteId).then((site) => { const currentUserId = site.getUserId(); // Prefetch the workshop data. - return this.getWorkshopInfoHelper(module, courseId, false, false, true, siteId).then((info) => { + return this.getWorkshopInfoHelper(module, courseId, commonOptions).then((info) => { const workshop = info.workshop, promises = [], assessments = []; promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id)); - promises.push(this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId) - .then((access) => { - return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => { + promises.push(this.workshopProvider.getWorkshopAccessInformation(workshop.id, modOptions).then((access) => { + return this.workshopProvider.getUserPlanPhases(workshop.id, modOptions).then((phases) => { // Get submission phase info. const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION], @@ -291,14 +311,14 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH promises2 = []; if (canSubmit) { - promises2.push(this.workshopProvider.getSubmissions(workshop.id)); + promises2.push(this.workshopProvider.getSubmissions(workshop.id, modOptions)); // Add userId to the profiles to prefetch. userIds.push(currentUserId); } let reportPromise = Promise.resolve(); if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { - reportPromise = this.getAllGradesReport(workshop.id, info.groups, siteId) + reportPromise = this.getAllGradesReport(workshop.id, info.groups, module.id, siteId) .then((grades) => { grades.forEach((grade) => { userIds.push(grade.userid); @@ -322,15 +342,19 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { // Wait the report promise to finish to override assessments array if needed. reportPromise = reportPromise.finally(() => { - return this.workshopHelper.getReviewerAssessments(workshop.id, currentUserId, undefined, - undefined, siteId).then((revAssessments) => { + return this.workshopHelper.getReviewerAssessments(workshop.id, { + userId: currentUserId, + cmId: module.id, + siteId, + }).then((revAssessments) => { const promises = []; let files = []; // Files in each submission. revAssessments.forEach((assessment) => { if (assessment.submission.authorid == currentUserId) { - promises.push(this.workshopProvider.getAssessment(workshop.id, assessment.id)); + promises.push(this.workshopProvider.getAssessment(workshop.id, assessment.id, + modOptions)); } userIds.push(assessment.reviewerid); userIds.push(assessment.gradinggradeoverby); @@ -350,17 +374,16 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH reportPromise = reportPromise.finally(() => { if (assessments.length > 0) { return Promise.all(assessments.map((assessment, id) => { - return this.workshopProvider.getAssessmentForm(workshop.id, id, undefined, undefined, undefined, - siteId); + return this.workshopProvider.getAssessmentForm(workshop.id, id, modOptions); })); } }); promises2.push(reportPromise); if (workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { - promises2.push(this.workshopProvider.getGrades(workshop.id)); + promises2.push(this.workshopProvider.getGrades(workshop.id, modOptions)); if (access.canviewpublishedsubmissions) { - promises2.push(this.workshopProvider.getSubmissions(workshop.id)); + promises2.push(this.workshopProvider.getSubmissions(workshop.id, modOptions)); } } @@ -391,3 +414,10 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH return this.syncProvider.syncWorkshop(module.instance, siteId); } } + +/** + * Options to pass to getWorkshopInfoHelper. + */ +export type AddonModWorkshopGetInfoOptions = CoreSitesCommonWSOptions & { + omitFail?: boolean; // True to always return even if fails. +}; diff --git a/src/addon/mod/workshop/providers/sync.ts b/src/addon/mod/workshop/providers/sync.ts index a7c87e2b4..905f21675 100644 --- a/src/addon/mod/workshop/providers/sync.ts +++ b/src/addon/mod/workshop/providers/sync.ts @@ -204,7 +204,7 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { return Promise.reject(null); } - return this.workshopProvider.getWorkshopById(courseId, workshopId, siteId).then((workshop) => { + return this.workshopProvider.getWorkshopById(courseId, workshopId, {siteId}).then((workshop) => { const submissionsActions = syncs[0], assessments = syncs[1], submissionEvaluations = syncs[2], @@ -289,7 +289,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { if (submissionId > 0) { editing = true; - timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => { + timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, { + cmId: workshop.coursemodule, + siteId, + }).then((submission) => { return submission.timemodified; }).catch(() => { return -1; @@ -403,7 +406,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { let discardError; const assessmentId = assessmentData.assessmentid; - const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => { + const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, { + cmId: workshop.coursemodule, + siteId, + }).then((assessment) => { return assessment.timemodified; }).catch(() => { return -1; @@ -481,7 +487,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { let discardError; const submissionId = evaluate.submissionid; - const timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => { + const timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, { + cmId: workshop.coursemodule, + siteId, + }).then((submission) => { return submission.timemodified; }).catch(() => { return -1; @@ -540,7 +549,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { let discardError; const assessmentId = evaluate.assessmentid; - const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => { + const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, { + cmId: workshop.coursemodule, + siteId, + }).then((assessment) => { return assessment.timemodified; }).catch(() => { return -1; diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index ec8fc292e..cf373a796 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -15,11 +15,12 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModWorkshopOfflineProvider } from './offline'; import { CoreSite } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for workshops. @@ -202,25 +203,21 @@ export class AddonModWorkshopProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the workshop is retrieved. */ - protected getWorkshopByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false): - Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getWorkshopByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] + courseids: [courseId], }; - const preSets: any = { + const preSets = { cacheKey: this.getWorkshopDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWorkshopProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (forceCache) { - preSets.omitExpires = true; - } - return site.read('mod_workshop_get_workshops_by_courses', params, preSets).then((response) => { if (response && response.workshops) { const workshopFound = response.workshops.find((workshop) => workshop[key] == value); @@ -252,12 +249,11 @@ export class AddonModWorkshopProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the workshop is retrieved. */ - getWorkshop(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getWorkshopByKey(courseId, 'coursemodule', cmId, siteId, forceCache); + getWorkshop(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWorkshopByKey(courseId, 'coursemodule', cmId, options); } /** @@ -265,12 +261,11 @@ export class AddonModWorkshopProvider { * * @param courseId Course ID. * @param id Workshop ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the workshop is retrieved. */ - getWorkshopById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getWorkshopByKey(courseId, 'id', id, siteId, forceCache); + getWorkshopById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWorkshopByKey(courseId, 'id', id, options); } /** @@ -303,28 +298,21 @@ export class AddonModWorkshopProvider { * Get access information for a given workshop. * * @param workshopId Workshop ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop is retrieved. */ - getWorkshopAccessInformation(workshopId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): - Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getWorkshopAccessInformation(workshopId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - workshopid: workshopId + workshopid: workshopId, }; - const preSets: any = { - cacheKey: this.getWorkshopAccessInformationDataCacheKey(workshopId) + const preSets = { + cacheKey: this.getWorkshopAccessInformationDataCacheKey(workshopId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_workshop_access_information', params, preSets); }); } @@ -346,28 +334,22 @@ export class AddonModWorkshopProvider { * Return the planner information for the given user. * * @param workshopId Workshop ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getUserPlanPhases(workshopId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getUserPlanPhases(workshopId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { workshopid: workshopId }; - const preSets: any = { + const preSets = { cacheKey: this.getUserPlanDataCacheKey(workshopId), - updateFrequency: CoreSite.FREQUENCY_OFTEN + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_user_plan', params, preSets).then((response) => { if (response && response.userplan && response.userplan.phases) { return this.utils.arrayToObject(response.userplan.phases, 'code'); @@ -395,33 +377,27 @@ export class AddonModWorkshopProvider { * Retrieves all the workshop submissions visible by the current user or the one done by the given user. * * @param workshopId Workshop ID. - * @param userId User ID, 0 means the current user. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop submissions are retrieved. */ - getSubmissions(workshopId: number, userId: number = 0, groupId: number = 0, offline: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getSubmissions(workshopId: number, options: AddonModWorkshopGetSubmissionsOptions = {}): Promise { + const userId = options.userId || 0; + const groupId = options.groupId || 0; + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { workshopid: workshopId, userid: userId, - groupid: groupId + groupid: groupId, }; - const preSets: any = { + const preSets = { cacheKey: this.getSubmissionsDataCacheKey(workshopId, userId, groupId), - updateFrequency: CoreSite.FREQUENCY_OFTEN + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_submissions', params, preSets).then((response) => { if (response && response.submissions) { return response.submissions; @@ -452,16 +428,19 @@ export class AddonModWorkshopProvider { * * @param workshopId Workshop ID. * @param submissionId Submission ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop submission data is retrieved. */ - getSubmission(workshopId: number, submissionId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getSubmission(workshopId: number, submissionId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - submissionid: submissionId + submissionid: submissionId, }; const preSets = { - cacheKey: this.getSubmissionDataCacheKey(workshopId, submissionId) + cacheKey: this.getSubmissionDataCacheKey(workshopId, submissionId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_workshop_get_submission', params, preSets).then((response) => { @@ -492,16 +471,19 @@ export class AddonModWorkshopProvider { * Returns the grades information for the given workshop and user. * * @param workshopId Workshop ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop grades data is retrieved. */ - getGrades(workshopId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getGrades(workshopId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { workshopid: workshopId }; const preSets = { - cacheKey: this.getGradesDataCacheKey(workshopId) + cacheKey: this.getGradesDataCacheKey(workshopId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_workshop_get_grades', params, preSets); @@ -525,35 +507,26 @@ export class AddonModWorkshopProvider { * Retrieves the assessment grades report. * * @param workshopId Workshop ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page Page of records to return. Default 0. - * @param perPage Records per page to return. Default AddonModWorkshopProvider.PER_PAGE. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getGradesReport(workshopId: number, groupId: number = 0, page: number = 0, perPage: number = 0, offline: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getGradesReport(workshopId: number, options: AddonModWorkshopGetGradesReportOptions = {}): Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { workshopid: workshopId, - groupid: groupId, - page: page, - perpage: perPage || AddonModWorkshopProvider.PER_PAGE + groupid: options.groupId, + page: options.page || 0, + perpage: options.perPage || AddonModWorkshopProvider.PER_PAGE }; - const preSets: any = { - cacheKey: this.getGradesReportDataCacheKey(workshopId, groupId), - updateFrequency: CoreSite.FREQUENCY_OFTEN + const preSets = { + cacheKey: this.getGradesReportDataCacheKey(workshopId, options.groupId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_grades_report', params, preSets).then((response) => { if (response && response.report) { return response.report; @@ -568,44 +541,37 @@ export class AddonModWorkshopProvider { * Performs the whole fetch of the grade reports in the workshop. * * @param workshopId Workshop ID. - * @param groupId Group ID. - * @param perPage Records per page to fetch. It has to match with the prefetch. - * Default on AddonModWorkshopProvider.PER_PAGE. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - fetchAllGradeReports(workshopId: number, groupId: number = 0, perPage: number = 0, forceCache: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - perPage = perPage || AddonModWorkshopProvider.PER_PAGE; - - return this.fetchGradeReportsRecursive(workshopId, groupId, perPage, forceCache, ignoreCache, [], 0, siteId); + fetchAllGradeReports(workshopId: number, options: AddonModWorkshopFetchAllGradesReportOptions = {}): Promise { + return this.fetchGradeReportsRecursive(workshopId, [], { + ...options, // Include all options. + page: 0, + perPage: options.perPage || AddonModWorkshopProvider.PER_PAGE, + siteId: options.siteId || this.sitesProvider.getCurrentSiteId(), + }); } /** * Recursive call on fetch all grade reports. * * @param workshopId Workshop ID. - * @param groupId Group ID. - * @param perPage Records per page to fetch. It has to match with the prefetch. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param grades Grades already fetched (just to concatenate them). - * @param page Page of records to return. - * @param siteId Site ID. + * @param options Other options. * @return Promise resolved when done. */ - protected fetchGradeReportsRecursive(workshopId: number, groupId: number, perPage: number, forceCache: boolean, - ignoreCache: boolean, grades: any[], page: number, siteId: string): Promise { - return this.getGradesReport(workshopId, groupId, page, perPage, forceCache, ignoreCache, siteId).then((report) => { + protected fetchGradeReportsRecursive(workshopId: number, grades: any[], options: AddonModWorkshopGetGradesReportOptions = {}) + : Promise { + + return this.getGradesReport(workshopId, options).then((report) => { Array.prototype.push.apply(grades, report.grades); - const canLoadMore = ((page + 1) * perPage) < report.totalcount; + const canLoadMore = ((options.page + 1) * options.perPage) < report.totalcount; if (canLoadMore) { - return this.fetchGradeReportsRecursive( - workshopId, groupId, perPage, forceCache, ignoreCache, grades, page + 1, siteId); + options.page++; + + return this.fetchGradeReportsRecursive(workshopId, grades, options); } return grades; @@ -631,28 +597,21 @@ export class AddonModWorkshopProvider { * * @param workshopId Workshop ID. * @param submissionId Submission ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getSubmissionAssessments(workshopId: number, submissionId: number, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getSubmissionAssessments(workshopId: number, submissionId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - submissionid: submissionId + submissionid: submissionId, }; - const preSets: any = { - cacheKey: this.getSubmissionAssessmentsDataCacheKey(workshopId, submissionId) + const preSets = { + cacheKey: this.getSubmissionAssessmentsDataCacheKey(workshopId, submissionId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_submission_assessments', params, preSets).then((response) => { if (response && response.assessments) { return response.assessments; @@ -898,31 +857,23 @@ export class AddonModWorkshopProvider { * Retrieves all the assessments reviewed by the given user. * * @param workshopId Workshop ID. - * @param userId User ID. If not defined, current user. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getReviewerAssessments(workshopId: number, userId?: number, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getReviewerAssessments(workshopId: number, options: AddonModWorkshopUserOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - workshopid: workshopId + workshopid: workshopId, }; - const preSets: any = { - cacheKey: this.getReviewerAssessmentsDataCacheKey(workshopId, userId) + const preSets = { + cacheKey: this.getReviewerAssessmentsDataCacheKey(workshopId, options.userId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (userId) { - params.userid = userId; - } - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; + if (options.userId) { + params.userid = options.userId; } return site.read('mod_workshop_get_reviewer_assessments', params, preSets).then((response) => { @@ -954,16 +905,19 @@ export class AddonModWorkshopProvider { * * @param workshopId Workshop ID. * @param assessmentId Assessment ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAssessment(workshopId: number, assessmentId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - assessmentid: assessmentId + assessmentid: assessmentId, }; const preSets = { - cacheKey: this.getAssessmentDataCacheKey(workshopId, assessmentId) + cacheKey: this.getAssessmentDataCacheKey(workshopId, assessmentId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_workshop_get_assessment', params, preSets).then((response) => { @@ -995,31 +949,26 @@ export class AddonModWorkshopProvider { * * @param workshopId Workshop ID. * @param assessmentId Assessment ID. - * @param mode Mode assessment (default) or preview. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getAssessmentForm(workshopId: number, assessmentId: number, mode: string = 'assessment', offline: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAssessmentForm(workshopId: number, assessmentId: number, options: AddonModWorkshopGetAssessmentFormOptions = {}) + : Promise { + const mode = options.mode || 'assessment'; + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { assessmentid: assessmentId, - mode: mode || 'assessment' + mode: mode, }; - const preSets: any = { + const preSets = { cacheKey: this.getAssessmentFormDataCacheKey(workshopId, assessmentId, mode), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_assessment_form_definition', params, preSets).then((response) => { if (response) { response.fields = this.parseFields(response.fields); @@ -1327,7 +1276,10 @@ export class AddonModWorkshopProvider { invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.getWorkshop(courseId, moduleId, siteId, true).then((workshop) => { + return this.getWorkshop(courseId, moduleId, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((workshop) => { return this.invalidateContentById(workshop.id, courseId, siteId); }); } @@ -1399,3 +1351,43 @@ export class AddonModWorkshopProvider { name, 'workshop', params, siteId); } } + +/** + * Common options with a user ID. + */ +export type AddonModWorkshopUserOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User ID. If not defined, current user. +}; + +/** + * Common options with a group ID. + */ +export type AddonModWorkshopGroupOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group id, 0 or not defined means that the function will determine the user group. +}; + +/** + * Options to pass to getSubmissions. + */ +export type AddonModWorkshopGetSubmissionsOptions = AddonModWorkshopUserOptions & AddonModWorkshopGroupOptions; + +/** + * Options to pass to fetchAllGradeReports. + */ +export type AddonModWorkshopFetchAllGradesReportOptions = AddonModWorkshopGroupOptions & { + perPage?: number; // Records per page to return. Default AddonModWorkshopProvider.PER_PAGE. +}; + +/** + * Options to pass to getGradesReport. + */ +export type AddonModWorkshopGetGradesReportOptions = AddonModWorkshopFetchAllGradesReportOptions & { + page?: number; // Page of records to return. Default 0. +}; + +/** + * Options to pass to getAssessmentForm. + */ +export type AddonModWorkshopGetAssessmentFormOptions = CoreCourseCommonModWSOptions & { + mode?: string; // Mode assessment (default) or preview. Defaults to 'assessment'. +}; diff --git a/src/addon/notifications/pages/list/list.html b/src/addon/notifications/pages/list/list.html index 951c63659..1bb618eaa 100644 --- a/src/addon/notifications/pages/list/list.html +++ b/src/addon/notifications/pages/list/list.html @@ -30,7 +30,7 @@

{{ notification.userfromfullname }}

-

+

diff --git a/src/addon/notifications/pages/list/list.scss b/src/addon/notifications/pages/list/list.scss index 122b8e4dc..15f586332 100644 --- a/src/addon/notifications/pages/list/list.scss +++ b/src/addon/notifications/pages/list/list.scss @@ -1,5 +1,62 @@ -page-addon-notifications-list .core-notification-icon { - width: 34px; - height: 34px; - margin: 10px !important; -} \ No newline at end of file +page-addon-notifications-list { + .core-notification-icon { + width: 34px; + height: 34px; + margin: 10px !important; + } + + .item core-format-text { + + .forumpost { + border: 1px solid $gray-light; + width: 100%; + margin: 0 0 1em 0; + + td { + padding: $content-padding; + } + + .header { + background-color: $gray-lighter; + } + + .picture { + width: auto; + text-align: center; + } + + .subject { + font-weight: 700; + margin-bottom: 1rem; + } + } + + a { + text-decoration: none; + } + + .userpicture { + border-radius: 50%; + } + + .mdl-right { + @include text-align('end'); + a { + display: none; + } + font { + font-size: 0.9em; + } + } + + .commands { + display: none; + } + + hr { + margin-top: 1.5rem; + margin-bottom: 1.5rem; + background-color: $gray-light; + } + } +} diff --git a/src/addon/notifications/pages/list/list.ts b/src/addon/notifications/pages/list/list.ts index 2046b00bc..49fbdb209 100644 --- a/src/addon/notifications/pages/list/list.ts +++ b/src/addon/notifications/pages/list/list.ts @@ -204,8 +204,21 @@ export class AddonNotificationsListPage { * @param notification The notification object. */ protected formatText(notification: AddonNotificationsAnyNotification): void { - const text = notification.mobiletext.replace(/-{4,}/ig, ''); - notification.mobiletext = this.textUtils.replaceNewLines(text, '
'); + notification.displayfullhtml = this.shouldDisplayFullHtml(notification); + + notification.mobiletext = notification.displayfullhtml ? + notification.fullmessagehtml : + this.textUtils.replaceNewLines(notification.mobiletext.replace(/-{4,}/ig, ''), '
'); + } + + /** + * Check whether we should display full HTML of the notification. + * + * @param notification Notification. + * @return Whether to display full HTML. + */ + protected shouldDisplayFullHtml(notification: AddonNotificationsAnyNotification): boolean { + return notification.component == 'mod_forum' && notification.eventtype == 'digests'; } /** diff --git a/src/addon/notifications/providers/notifications.ts b/src/addon/notifications/providers/notifications.ts index 3a2234ada..8a96d1f68 100644 --- a/src/addon/notifications/providers/notifications.ts +++ b/src/addon/notifications/providers/notifications.ts @@ -616,4 +616,5 @@ export type AddonNotificationsNotificationCalculatedData = { courseid?: number; // Calculated in the app. Course the notification belongs to. profileimageurlfrom?: string; // Calculated in the app. Avatar of user that sent the notification. userfromfullname?: string; // Calculated in the app in some cases. User from full name. + displayfullhtml?: boolean; // Whether to display the full HTML of the notification. }; diff --git a/src/addon/notifications/providers/push-click-handler.ts b/src/addon/notifications/providers/push-click-handler.ts index 425437ddb..e95c9440d 100644 --- a/src/addon/notifications/providers/push-click-handler.ts +++ b/src/addon/notifications/providers/push-click-handler.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; +import { CoreTextUtils } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CorePushNotificationsClickHandler } from '@core/pushnotifications/providers/delegate'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; @@ -38,6 +39,11 @@ export class AddonNotificationsPushClickHandler implements CorePushNotifications * @return Whether the notification click is handled by this handler */ handles(notification: any): boolean | Promise { + if (!notification.moodlecomponent) { + // The notification doesn't come from Moodle. Handle it. + return true; + } + if (this.utils.isTrueOrOne(notification.notif)) { // Notification clicked, mark as read. Don't block for this. const notifId = notification.savedmessageid || notification.id; @@ -60,38 +66,46 @@ export class AddonNotificationsPushClickHandler implements CorePushNotifications * @param notification The notification to check. * @return Promise resolved when done. */ - handleClick(notification: any): Promise { - let promise; + async handleClick(notification: any): Promise { - // Try to handle the appurl first. - if (notification.customdata && notification.customdata.appurl) { - promise = this.linkHelper.handleLink(notification.customdata.appurl, undefined, undefined, true); - } else { - promise = Promise.resolve(false); + if (notification.customdata.extendedtext) { + // Display the text in a modal. + return CoreTextUtils.instance.viewText(notification.title, notification.customdata.extendedtext, { + displayCopyButton: true, + modalOptions: { cssClass: 'core-modal-fullscreen' }, + }); } - return promise.then((treated) => { + // Try to handle the appurl. + if (notification.customdata && notification.customdata.appurl) { + switch (notification.customdata.appurlopenin) { + case 'inapp': + this.utils.openInApp(notification.customdata.appurl); - if (!treated) { - // No link or cannot be handled by the app. Try to handle the contexturl now. - if (notification.contexturl) { - return this.linkHelper.handleLink(notification.contexturl); - } else { - return false; - } + return; + + case 'browser': + return this.utils.openInBrowser(notification.customdata.appurl); + + default: + if (this.linkHelper.handleLink(notification.customdata.appurl, undefined, undefined, true)) { + // Link treated, stop. + return; + } } + } - return true; - }).then((treated) => { - - if (!treated) { - // No link or cannot be handled by the app. Open the notifications page. - return this.notificationsProvider.invalidateNotificationsList(notification.site).catch(() => { - // Ignore errors. - }).then(() => { - return this.linkHelper.goInSite(undefined, 'AddonNotificationsListPage', undefined, notification.site); - }); + // No appurl or cannot be handled by the app. Try to handle the contexturl now. + if (notification.contexturl) { + if (this.linkHelper.handleLink(notification.contexturl)) { + // Link treated, stop. + return; } - }); + } + + // No contexturl or cannot be handled by the app. Open the notifications page. + await this.utils.ignoreErrors(this.notificationsProvider.invalidateNotificationsList(notification.site)); + + await this.linkHelper.goInSite(undefined, 'AddonNotificationsListPage', undefined, notification.site); } } diff --git a/src/addon/qbehaviour/deferredcbm/providers/handler.ts b/src/addon/qbehaviour/deferredcbm/providers/handler.ts index a32634c1f..1ba3bcdcc 100644 --- a/src/addon/qbehaviour/deferredcbm/providers/handler.ts +++ b/src/addon/qbehaviour/deferredcbm/providers/handler.ts @@ -40,13 +40,14 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH * @param component Component the question belongs to. * @param attemptId Attempt ID the question belongs to. * @param question The question. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { // Depends on deferredfeedback. - return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, siteId, + return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, componentId, siteId, this.isCompleteResponse.bind(this), this.isSameResponse.bind(this)); } @@ -71,11 +72,13 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - protected isCompleteResponse(question: any, answers: any): number { + protected isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // First check if the question answer is complete. - const complete = this.questionDelegate.isCompleteResponse(question, answers); + const complete = this.questionDelegate.isCompleteResponse(question, answers, component, componentId); if (complete > 0) { // Answer is complete, check the user answered CBM too. return answers['-certainty'] ? 1 : 0; @@ -101,12 +104,14 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH * @param prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...). * @param newAnswers Object with the new question answers. * @param newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...). + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - protected isSameResponse(question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, newBasicAnswers: any) - : boolean { + protected isSameResponse(question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, newBasicAnswers: any, + component: string, componentId: string | number): boolean { // First check if the question answer is the same. - const same = this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers); + const same = this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId); if (same) { // Same response, check the CBM is the same too. return prevAnswers['-certainty'] == newAnswers['-certainty']; diff --git a/src/addon/qbehaviour/deferredfeedback/providers/handler.ts b/src/addon/qbehaviour/deferredfeedback/providers/handler.ts index 0edf35d41..31f21252a 100644 --- a/src/addon/qbehaviour/deferredfeedback/providers/handler.ts +++ b/src/addon/qbehaviour/deferredfeedback/providers/handler.ts @@ -23,9 +23,11 @@ import { CoreQuestionProvider, CoreQuestionState } from '@core/question/provider * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ -export type isCompleteResponseFunction = (question: any, answers: any) => number; +export type isCompleteResponseFunction = (question: any, answers: any, component: string, componentId: string | number) => number; /** * Check if two responses are the same. @@ -35,10 +37,12 @@ export type isCompleteResponseFunction = (question: any, answers: any) => number * @param prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...). * @param newAnswers Object with the new question answers. * @param newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...). + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ export type isSameResponseFunction = (question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, - newBasicAnswers: any) => boolean; + newBasicAnswers: any, component: string, componentId: string | number) => boolean; /** * Handler to support deferred feedback question behaviour. @@ -58,12 +62,13 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav * @param component Component the question belongs to. * @param attemptId Attempt ID the question belongs to. * @param question The question. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { - return this.determineNewStateDeferred(component, attemptId, question, siteId); + return this.determineNewStateDeferred(component, attemptId, question, componentId, siteId); } /** @@ -72,75 +77,82 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav * @param component Component the question belongs to. * @param attemptId Attempt ID the question belongs to. * @param question The question. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @param isCompleteFn Function to override the default isCompleteResponse check. * @param isSameFn Function to override the default isSameResponse check. * @return Promise resolved with state. */ - determineNewStateDeferred(component: string, attemptId: number, question: any, siteId?: string, - isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction): Promise { + async determineNewStateDeferred(component: string, attemptId: number, question: any, componentId: string | number, + siteId?: string, isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction) + : Promise { // Check if we have local data for the question. - return this.questionProvider.getQuestion(component, attemptId, question.slot, siteId).catch(() => { + let dbQuestion; + try { + dbQuestion = await this.questionProvider.getQuestion(component, attemptId, question.slot, siteId); + } catch (error) { // No entry found, use the original data. - return question; - }).then((dbQuestion) => { - const state = this.questionProvider.getState(dbQuestion.state); + dbQuestion = question; + } - if (state.finished || !state.active) { - // Question is finished, it cannot change. - return state; + const state = this.questionProvider.getState(dbQuestion.state); + + if (state.finished || !state.active) { + // Question is finished, it cannot change. + return state; + } + + const newBasicAnswers = this.questionProvider.getBasicAnswers(question.answers); + + if (dbQuestion.state) { + // Question already has a state stored. Check if answer has changed. + let prevAnswers = await this.questionProvider.getQuestionAnswers(component, attemptId, question.slot, false, siteId); + + prevAnswers = this.questionProvider.convertAnswersArrayToObject(prevAnswers, true); + const prevBasicAnswers = this.questionProvider.getBasicAnswers(prevAnswers); + + // If answers haven't changed the state is the same. + if (isSameFn) { + if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers, + component, componentId)) { + return state; + } + } else { + if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId)) { + return state; + } } + } - // We need to check if the answers have changed. Retrieve current stored answers. - return this.questionProvider.getQuestionAnswers(component, attemptId, question.slot, false, siteId) - .then((prevAnswers) => { + // Answers have changed. Now check if the response is complete and calculate the new state. + let complete: number; + let newState: string; - const newBasicAnswers = this.questionProvider.getBasicAnswers(question.answers); + if (isCompleteFn) { + // Pass all the answers since some behaviours might need the extra data. + complete = isCompleteFn(question, question.answers, component, componentId); + } else { + // Only pass the basic answers since questions should be independent of extra data. + complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers, component, componentId); + } - prevAnswers = this.questionProvider.convertAnswersArrayToObject(prevAnswers, true); - const prevBasicAnswers = this.questionProvider.getBasicAnswers(prevAnswers); + if (complete < 0) { + newState = 'cannotdeterminestatus'; + } else if (complete > 0) { + newState = 'complete'; + } else { + const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers, component, componentId); + if (gradable < 0) { + newState = 'cannotdeterminestatus'; + } else if (gradable > 0) { + newState = 'invalid'; + } else { + newState = 'todo'; + } + } - // If answers haven't changed the state is the same. - if (isSameFn) { - if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) { - return state; - } - } else { - if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) { - return state; - } - } - - // Answers have changed. Now check if the response is complete and calculate the new state. - let complete: number, - newState: string; - if (isCompleteFn) { - // Pass all the answers since some behaviours might need the extra data. - complete = isCompleteFn(question, question.answers); - } else { - // Only pass the basic answers since questions should be independent of extra data. - complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers); - } - - if (complete < 0) { - newState = 'cannotdeterminestatus'; - } else if (complete > 0) { - newState = 'complete'; - } else { - const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers); - if (gradable < 0) { - newState = 'cannotdeterminestatus'; - } else if (gradable > 0) { - newState = 'invalid'; - } else { - newState = 'todo'; - } - } - - return this.questionProvider.getState(newState); - }); - }); + return this.questionProvider.getState(newState); } /** diff --git a/src/addon/qbehaviour/informationitem/providers/handler.ts b/src/addon/qbehaviour/informationitem/providers/handler.ts index c30e2c800..dfe438725 100644 --- a/src/addon/qbehaviour/informationitem/providers/handler.ts +++ b/src/addon/qbehaviour/informationitem/providers/handler.ts @@ -35,10 +35,11 @@ export class AddonQbehaviourInformationItemHandler implements CoreQuestionBehavi * @param component Component the question belongs to. * @param attemptId Attempt ID the question belongs to. * @param question The question. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { if (question.answers['-seen']) { return this.questionProvider.getState('complete'); diff --git a/src/addon/qbehaviour/manualgraded/providers/handler.ts b/src/addon/qbehaviour/manualgraded/providers/handler.ts index 98da9c06d..9a650d9d2 100644 --- a/src/addon/qbehaviour/manualgraded/providers/handler.ts +++ b/src/addon/qbehaviour/manualgraded/providers/handler.ts @@ -17,28 +17,7 @@ import { Injectable } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question'; - -/** - * Check if a response is complete. - * - * @param question The question. - * @param answers Object with the question answers (without prefix). - * @return 1 if complete, 0 if not complete, -1 if cannot determine. - */ -export type isCompleteResponseFunction = (question: any, answers: any) => number; - -/** - * Check if two responses are the same. - * - * @param question Question. - * @param prevAnswers Object with the previous question answers. - * @param prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...). - * @param newAnswers Object with the new question answers. - * @param newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...). - * @return Whether they're the same. - */ -export type isSameResponseFunction = (question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, - newBasicAnswers: any) => boolean; +import { AddonQbehaviourDeferredFeedbackHandler } from '@addon/qbehaviour/deferredfeedback/providers/handler'; /** * Handler to support manual graded question behaviour. @@ -48,7 +27,9 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour name = 'AddonQbehaviourManualGraded'; type = 'manualgraded'; - constructor(private questionDelegate: CoreQuestionDelegate, private questionProvider: CoreQuestionProvider) { + constructor(protected questionDelegate: CoreQuestionDelegate, + protected questionProvider: CoreQuestionProvider, + protected deferredFeedbackHandler: AddonQbehaviourDeferredFeedbackHandler) { // Nothing to do. } @@ -58,82 +39,14 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour * @param component Component the question belongs to. * @param attemptId Attempt ID the question belongs to. * @param question The question. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { - return this.determineNewStateManualGraded(component, attemptId, question, siteId); - } - - /** - * Determine a question new state based on its answer(s) for manual graded question behaviour. - * - * @param component Component the question belongs to. - * @param attemptId Attempt ID the question belongs to. - * @param question The question. - * @param siteId Site ID. If not defined, current site. - * @param isCompleteFn Function to override the default isCompleteResponse check. - * @param isSameFn Function to override the default isSameResponse check. - * @return Promise resolved with state. - */ - determineNewStateManualGraded(component: string, attemptId: number, question: any, siteId?: string, - isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction): Promise { - - // Check if we have local data for the question. - return this.questionProvider.getQuestion(component, attemptId, question.slot, siteId).catch(() => { - // No entry found, use the original data. - return question; - }).then((dbQuestion) => { - const state = this.questionProvider.getState(dbQuestion.state); - - if (state.finished || !state.active) { - // Question is finished, it cannot change. - return state; - } - - // We need to check if the answers have changed. Retrieve current stored answers. - return this.questionProvider.getQuestionAnswers(component, attemptId, question.slot, false, siteId) - .then((prevAnswers) => { - - const newBasicAnswers = this.questionProvider.getBasicAnswers(question.answers); - - prevAnswers = this.questionProvider.convertAnswersArrayToObject(prevAnswers, true); - const prevBasicAnswers = this.questionProvider.getBasicAnswers(prevAnswers); - - // If answers haven't changed the state is the same. - if (isSameFn) { - if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) { - return state; - } - } else { - if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) { - return state; - } - } - - // Answers have changed. Now check if the response is complete and calculate the new state. - let complete: number, - newState: string; - if (isCompleteFn) { - // Pass all the answers since some behaviours might need the extra data. - complete = isCompleteFn(question, question.answers); - } else { - // Only pass the basic answers since questions should be independent of extra data. - complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers); - } - - if (complete < 0) { - newState = 'cannotdeterminestatus'; - } else if (complete > 0) { - newState = 'complete'; - } else { - newState = 'todo'; - } - - return this.questionProvider.getState(newState); - }); - }); + // Same implementation as the deferred feedback. Use that function instead of replicating it. + return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, componentId, siteId); } /** diff --git a/src/addon/qtype/calculated/providers/handler.ts b/src/addon/qtype/calculated/providers/handler.ts index c7832f91c..0e432658f 100644 --- a/src/addon/qtype/calculated/providers/handler.ts +++ b/src/addon/qtype/calculated/providers/handler.ts @@ -24,6 +24,14 @@ import { AddonQtypeCalculatedComponent } from '../component/calculated'; */ @Injectable() export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { + static UNITINPUT = '0'; + static UNITRADIO = '1'; + static UNITSELECT = '2'; + static UNITNONE = '3'; + + static UNITGRADED = '1'; + static UNITOPTIONAL = '0'; + name = 'AddonQtypeCalculated'; type = 'qtype_calculated'; @@ -41,23 +49,69 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { return AddonQtypeCalculatedComponent; } + /** + * Check if the units are in a separate field for the question. + * + * @param question Question. + * @return Whether units are in a separate field. + */ + hasSeparateUnitField(question: any): boolean { + if (!question.settings) { + const element = this.domUtils.convertToElement(question.html); + + return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]')); + } + + return question.settings.unitdisplay === AddonQtypeCalculatedHandler.UNITRADIO || + question.settings.unitdisplay === AddonQtypeCalculatedHandler.UNITSELECT; + } + /** * Check if a response is complete. * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { - if (this.isGradableResponse(question, answers) === 0 || !this.validateUnits(answers['answer'])) { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { + if (!this.isGradableResponse(question, answers, component, componentId)) { return 0; } - if (this.requiresUnits(question)) { - return this.isValidValue(answers['unit']) ? 1 : 0; + const parsedAnswer = this.parseAnswer(question, answers['answer']); + if (parsedAnswer.answer === null) { + return 0; } - return -1; + if (!question.settings) { + if (this.hasSeparateUnitField(question)) { + return this.isValidValue(answers['unit']) ? 1 : 0; + } + + // We cannot know if the answer should contain units or not. + return -1; + } + + if (question.settings.unitdisplay != AddonQtypeCalculatedHandler.UNITINPUT && parsedAnswer.unit) { + // There should be no units or be outside of the input, not valid. + return 0; + } + + if (this.hasSeparateUnitField(question) && !this.isValidValue(answers['unit'])) { + // Unit not supplied as a separate field and it's required. + return 0; + } + + if (question.settings.unitdisplay == AddonQtypeCalculatedHandler.UNITINPUT && + question.settings.unitgradingtype == AddonQtypeCalculatedHandler.UNITGRADED && + !this.isValidValue(parsedAnswer.unit)) { + // Unit not supplied inside the input and it's required. + return 0; + } + + return 1; } /** @@ -75,16 +129,12 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - let isGradable = this.isValidValue(answers['answer']); - if (isGradable && this.requiresUnits(question)) { - // The question requires a unit. - isGradable = this.isValidValue(answers['unit']); - } - - return isGradable ? 1 : 0; + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + return this.isValidValue(answers['answer']) ? 1 : 0; } /** @@ -93,9 +143,11 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') && this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit'); } @@ -111,36 +163,24 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { } /** - * Check if a question requires units in a separate input. - * - * @param question The question. - * @return Whether the question requires units. - */ - requiresUnits(question: any): boolean { - const element = this.domUtils.convertToElement(question.html); - - return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]')); - } - - /** - * Validate a number with units. We don't have the list of valid units and conversions, so we can't perform - * a full validation. If this function returns true it means we can't be sure it's valid. + * Parse an answer string. * + * @param question Question. * @param answer Answer. - * @return False if answer isn't valid, true if we aren't sure if it's valid. + * @return 0 if answer isn't valid, 1 if answer is valid, -1 if we aren't sure if it's valid. */ - validateUnits(answer: string): boolean { + parseAnswer(question: any, answer: string): {answer: number, unit: string} { if (!answer) { - return false; + return {answer: null, unit: null}; } - const regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?'; + let regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?'; // Strip spaces (which may be thousands separators) and change other forms of writing e to e. - answer = answer.replace(' ', ''); + answer = answer.replace(/ /g, ''); answer = answer.replace(/(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)/, 'e$1'); - // If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and stip it. + // If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and strip it. // Else assume it is a decimal separator, and change it to '.'. if (answer.indexOf('.') != -1 || answer.split(',').length - 1 > 1) { answer = answer.replace(',', ''); @@ -148,11 +188,31 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { answer = answer.replace(',', '.'); } - // We don't know if units should be before or after so we check both. - if (answer.match(new RegExp('^' + regexString)) === null || answer.match(new RegExp(regexString + '$')) === null) { - return false; + let unitsLeft = false; + let match = null; + + if (!question.settings || question.settings.unitsleft === null) { + // We don't know if units should be before or after so we check both. + match = answer.match(new RegExp('^' + regexString)); + if (!match) { + unitsLeft = true; + match = answer.match(new RegExp(regexString + '$')); + } + } else { + unitsLeft = question.settings.unitsleft == '1'; + regexString = unitsLeft ? regexString + '$' : '^' + regexString; + + match = answer.match(new RegExp(regexString)); } - return true; + if (!match) { + return {answer: null, unit: null}; + } + + const numberString = match[0]; + const unit = unitsLeft ? answer.substr(0, answer.length - match[0].length) : answer.substr(match[0].length); + + // No need to calculate the multiplier. + return {answer: Number(numberString), unit: unit}; } } diff --git a/src/addon/qtype/calculatedmulti/providers/handler.ts b/src/addon/qtype/calculatedmulti/providers/handler.ts index 63fcd7a3e..e2d8111ca 100644 --- a/src/addon/qtype/calculatedmulti/providers/handler.ts +++ b/src/addon/qtype/calculatedmulti/providers/handler.ts @@ -46,9 +46,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question type depends on multichoice. return this.multichoiceHandler.isCompleteResponseSingle(answers); } @@ -68,9 +70,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question type depends on multichoice. return this.multichoiceHandler.isGradableResponseSingle(answers); } @@ -81,9 +85,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { // This question type depends on multichoice. return this.multichoiceHandler.isSameResponseSingle(prevAnswers, newAnswers); } diff --git a/src/addon/qtype/calculatedsimple/providers/handler.ts b/src/addon/qtype/calculatedsimple/providers/handler.ts index e5f442b20..1301f3da5 100644 --- a/src/addon/qtype/calculatedsimple/providers/handler.ts +++ b/src/addon/qtype/calculatedsimple/providers/handler.ts @@ -46,11 +46,13 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question type depends on calculated. - return this.calculatedHandler.isCompleteResponse(question, answers); + return this.calculatedHandler.isCompleteResponse(question, answers, component, componentId); } /** @@ -68,11 +70,13 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question type depends on calculated. - return this.calculatedHandler.isGradableResponse(question, answers); + return this.calculatedHandler.isGradableResponse(question, answers, component, componentId); } /** @@ -81,10 +85,12 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { // This question type depends on calculated. - return this.calculatedHandler.isSameResponse(question, prevAnswers, newAnswers); + return this.calculatedHandler.isSameResponse(question, prevAnswers, newAnswers, component, componentId); } } diff --git a/src/addon/qtype/ddimageortext/providers/handler.ts b/src/addon/qtype/ddimageortext/providers/handler.ts index 1774415fb..94dcebcf6 100644 --- a/src/addon/qtype/ddimageortext/providers/handler.ts +++ b/src/addon/qtype/ddimageortext/providers/handler.ts @@ -61,9 +61,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // An answer is complete if all drop zones have an answer. // We should always receive all the drop zones with their value ('' if not answered). for (const name in answers) { @@ -91,9 +93,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { for (const name in answers) { const value = answers[name]; if (value && value !== '0') { @@ -110,9 +114,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } } diff --git a/src/addon/qtype/ddmarker/classes/ddmarker.ts b/src/addon/qtype/ddmarker/classes/ddmarker.ts index 90b5686b1..0379c7322 100644 --- a/src/addon/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addon/qtype/ddmarker/classes/ddmarker.ts @@ -751,7 +751,7 @@ export class AddonQtypeDdMarkerQuestion { this.question.loaded = true; }; - if (bgImg.complete && bgImg.naturalWidth) { + if (!bgImg.src || (bgImg.complete && bgImg.naturalWidth)) { imgLoaded(); return; diff --git a/src/addon/qtype/ddmarker/providers/handler.ts b/src/addon/qtype/ddmarker/providers/handler.ts index 2e8195ff5..a3c2bd01e 100644 --- a/src/addon/qtype/ddmarker/providers/handler.ts +++ b/src/addon/qtype/ddmarker/providers/handler.ts @@ -18,6 +18,7 @@ import { CoreQuestionProvider } from '@core/question/providers/question'; import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { AddonQtypeDdMarkerComponent } from '../component/ddmarker'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Handler to support drag-and-drop markers question type. @@ -62,9 +63,11 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // If 1 dragitem is set we assume the answer is complete (like Moodle does). for (const name in answers) { if (answers[name]) { @@ -90,10 +93,12 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers); + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + return this.isCompleteResponse(question, answers, component, componentId); } /** @@ -102,9 +107,11 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } @@ -113,12 +120,12 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles(question: any, usageId: number): string[] { + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { this.questionHelper.extractQuestionScripts(question, usageId); - if (question.amdArgs && typeof question.amdArgs[1] !== 'undefined') { + if (question.amdArgs && typeof question.amdArgs[1] == 'string') { // Moodle 3.6+. return [question.amdArgs[1]]; } diff --git a/src/addon/qtype/ddwtos/providers/handler.ts b/src/addon/qtype/ddwtos/providers/handler.ts index d310e27b2..8f740bea7 100644 --- a/src/addon/qtype/ddwtos/providers/handler.ts +++ b/src/addon/qtype/ddwtos/providers/handler.ts @@ -61,9 +61,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { for (const name in answers) { const value = answers[name]; if (!value || value === '0') { @@ -89,9 +91,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { for (const name in answers) { const value = answers[name]; if (value && value !== '0') { @@ -108,9 +112,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } } diff --git a/src/addon/qtype/essay/component/addon-qtype-essay.html b/src/addon/qtype/essay/component/addon-qtype-essay.html index 13462423b..0564f70b1 100644 --- a/src/addon/qtype/essay/component/addon-qtype-essay.html +++ b/src/addon/qtype/essay/component/addon-qtype-essay.html @@ -4,39 +4,50 @@

- - - - - - - - - + + + + + + + + + + + + - - - -

{{ 'core.question.errorinlinefilesnotsupported' | translate }}

-
- -

-
+ + + +

{{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }}

+
+ +

+
+
+ + + + + + + + + +

{{ 'core.question.errorattachmentsnotsupportedinsite' | translate }}

+
+
- - -

{{ 'core.question.errorattachmentsnotsupported' | translate }}

-
+ + + + +

+
- - -

-
- - -
- -
-
+ + +
diff --git a/src/addon/qtype/essay/component/essay.ts b/src/addon/qtype/essay/component/essay.ts index 8cd380933..aee330d49 100644 --- a/src/addon/qtype/essay/component/essay.ts +++ b/src/addon/qtype/essay/component/essay.ts @@ -14,8 +14,12 @@ import { Component, OnInit, Injector } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; +import { CoreWSExternalFile } from '@providers/ws'; +import { CoreFileUploader } from '@core/fileuploader/providers/fileuploader'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; +import { CoreQuestion } from '@core/question/providers/question'; import { FormControl, FormBuilder } from '@angular/forms'; +import { CoreFileSession } from '@providers/file-session'; /** * Component to render an essay question. @@ -28,6 +32,9 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen protected formControl: FormControl; + attachments: CoreWSExternalFile[]; + uploadFilesSupported: boolean; + constructor(logger: CoreLoggerProvider, injector: Injector, protected fb: FormBuilder) { super(logger, 'AddonQtypeEssayComponent', injector); } @@ -36,8 +43,39 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen * Component being initialized. */ ngOnInit(): void { - this.initEssayComponent(); + this.uploadFilesSupported = typeof this.question.responsefileareas != 'undefined'; + this.initEssayComponent(this.review); this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text); + + if (this.question.allowsAttachments && this.uploadFilesSupported && !this.review) { + this.loadAttachments(); + } + } + + /** + * Load attachments. + * + * @return Promise resolved when done. + */ + async loadAttachments(): Promise { + if (this.offlineEnabled && this.question.localAnswers['attachments_offline']) { + + const attachmentsData = this.textUtils.parseJSON(this.question.localAnswers['attachments_offline'], {}); + let offlineFiles = []; + + if (attachmentsData.offline) { + offlineFiles = await this.questionHelper.getStoredQuestionFiles(this.question, this.component, this.componentId); + + offlineFiles = CoreFileUploader.instance.markOfflineFiles(offlineFiles); + } + + this.attachments = (attachmentsData.online || []).concat(offlineFiles); + } else { + this.attachments = Array.from(this.questionHelper.getResponseFileAreaFiles(this.question, 'attachments')); + } + + CoreFileSession.instance.setFiles(this.component, + CoreQuestion.instance.getQuestionComponentId(this.question, this.componentId), this.attachments); } } diff --git a/src/addon/qtype/essay/providers/handler.ts b/src/addon/qtype/essay/providers/handler.ts index 9206b4e4e..834850ca4 100644 --- a/src/addon/qtype/essay/providers/handler.ts +++ b/src/addon/qtype/essay/providers/handler.ts @@ -14,12 +14,17 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; +import { CoreFileSession } from '@providers/file-session'; +import { CoreSites } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreFileUploader } from '@core/fileuploader/providers/fileuploader'; import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { CoreQuestion } from '@core/question/providers/question'; import { AddonQtypeEssayComponent } from '../component/essay'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Handler to support essay question type. @@ -32,6 +37,76 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { constructor(private utils: CoreUtilsProvider, private questionHelper: CoreQuestionHelperProvider, private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider) { } + /** + * Clear temporary data after the data has been saved. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + */ + clearTmpData(question: any, component: string, componentId: string | number): void { + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const files = CoreFileSession.instance.getFiles(component, questionComponentId); + + // Clear the files in session for this question. + CoreFileSession.instance.clearFiles(component, questionComponentId); + + // Now delete the local files from the tmp folder. + CoreFileUploader.instance.clearTmpFiles(files); + } + + /** + * Delete any stored data for the question. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + deleteOfflineData(question: any, component: string, componentId: string | number, siteId?: string): Promise { + return this.questionHelper.deleteStoredQuestionFiles(question, component, componentId, siteId); + } + + /** + * Get the list of files that needs to be downloaded in addition to the files embedded in the HTML. + * + * @param question Question. + * @param usageId Usage ID. + * @return List of files or URLs. + */ + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { + if (!question.responsefileareas) { + return []; + } + + return question.responsefileareas.reduce((urlsList, area) => { + return urlsList.concat(area.files || []); + }, []); + } + + /** + * Check whether the question allows text and/or attachments. + * + * @param question Question to check. + * @return Allowed options. + */ + protected getAllowedOptions(question: any): {text: boolean, attachments: boolean} { + if (question.settings) { + return { + text: question.settings.responseformat != 'noinline', + attachments: question.settings.attachments != '0', + }; + } else { + const element = this.domUtils.convertToElement(question.html); + + return { + text: !!element.querySelector('textarea[name*=_answer]'), + attachments: !!element.querySelector('div[id*=filemanager]'), + }; + } + } + /** * Return the name of the behaviour to use for the question. * If the question should use the default behaviour you shouldn't implement this function. @@ -65,14 +140,15 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { */ getPreventSubmitMessage(question: any): string { const element = this.domUtils.convertToElement(question.html); + const uploadFilesSupported = typeof question.responsefileareas != 'undefined'; - if (element.querySelector('div[id*=filemanager]')) { + if (!uploadFilesSupported && element.querySelector('div[id*=filemanager]')) { // The question allows attachments. Since the app cannot attach files yet we will prevent submitting the question. - return 'core.question.errorattachmentsnotsupported'; + return 'core.question.errorattachmentsnotsupportedinsite'; } - if (this.questionHelper.hasDraftFileUrls(element.innerHTML)) { - return 'core.question.errorinlinefilesnotsupported'; + if (!uploadFilesSupported && this.questionHelper.hasDraftFileUrls(element.innerHTML)) { + return 'core.question.errorembeddedfilesnotsupportedinsite'; } } @@ -81,20 +157,34 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { - const element = this.domUtils.convertToElement(question.html); + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { - const hasInlineText = answers['answer'] && answers['answer'] !== '', - allowsAttachments = !!element.querySelector('div[id*=filemanager]'); + const hasTextAnswer = answers['answer'] && answers['answer'] !== ''; + const uploadFilesSupported = typeof question.responsefileareas != 'undefined'; + const allowedOptions = this.getAllowedOptions(question); - if (!allowsAttachments) { - return hasInlineText ? 1 : 0; + if (!allowedOptions.attachments) { + return hasTextAnswer ? 1 : 0; } - // We can't know if the attachments are required or if the user added any in web. - return -1; + if (!uploadFilesSupported) { + // We can't know if the attachments are required or if the user added any in web. + return -1; + } + + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); + + if (!allowedOptions.text) { + return attachments && attachments.length >= Number(question.settings.attachmentsrequired) ? 1 : 0; + } + + return ((hasTextAnswer || question.settings.responserequired == '0') && + (attachments && attachments.length >= Number(question.settings.attachmentsrequired))) ? 1 : 0; } /** @@ -112,10 +202,20 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - return 0; + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + if (typeof question.responsefileareas == 'undefined') { + return -1; + } + + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); + + // Determine if the given response has online text or attachments. + return (answers['answer'] && answers['answer'] !== '') || (attachments && attachments.length > 0) ? 1 : 0; } /** @@ -124,28 +224,175 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { - return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { + const uploadFilesSupported = typeof question.responsefileareas != 'undefined'; + const allowedOptions = this.getAllowedOptions(question); + + // First check the inline text. + const answerIsEqual = allowedOptions.text ? this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') : true; + + if (!allowedOptions.attachments || !uploadFilesSupported || !answerIsEqual) { + // No need to check attachments. + return answerIsEqual; + } + + // Check attachments now. + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); + const originalAttachments = this.questionHelper.getResponseFileAreaFiles(question, 'attachments'); + + return !CoreFileUploader.instance.areFileListDifferent(attachments, originalAttachments); } /** - * Prepare and add to answers the data to send to server based in the input. Return promise if async. + * Prepare and add to answers the data to send to server based in the input. * * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + async prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, + siteId?: string): Promise { + const element = this.domUtils.convertToElement(question.html); + const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]'); // Search the textarea to get its name. const textarea = element.querySelector('textarea[name*=_answer]'); if (textarea && typeof answers[textarea.name] != 'undefined') { + await this.prepareTextAnswer(question, answers, textarea, siteId); + } + + if (attachmentsInput) { + await this.prepareAttachments(question, answers, offline, component, componentId, attachmentsInput, siteId); + } + } + + /** + * Prepare attachments. + * + * @param question Question. + * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. + * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param attachmentsInput The HTML input containing the draft ID for attachments. + * @param siteId Site ID. If not defined, current site. + * @return Return a promise resolved when done if async, void if sync. + */ + async prepareAttachments(question: any, answers: any, offline: boolean, component: string, componentId: string | number, + attachmentsInput: HTMLInputElement, siteId?: string): Promise { + + // Treat attachments if any. + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); + const draftId = Number(attachmentsInput.value); + + if (offline) { + // Get the folder where to store the files. + const folderPath = CoreQuestion.instance.getQuestionFolder(question.type, component, questionComponentId, siteId); + + const result = await CoreFileUploader.instance.storeFilesToUpload(folderPath, attachments); + + // Store the files in the answers. + answers[attachmentsInput.name + '_offline'] = JSON.stringify(result); + } else { + // Check if any attachment was deleted. + const originalAttachments = this.questionHelper.getResponseFileAreaFiles(question, 'attachments'); + const filesToDelete = CoreFileUploader.instance.getFilesToDelete(originalAttachments, attachments); + + if (filesToDelete.length > 0) { + // Delete files. + await CoreFileUploader.instance.deleteDraftFiles(draftId, filesToDelete, siteId); + } + + await CoreFileUploader.instance.uploadFiles(draftId, attachments, siteId); + } + } + + /** + * Prepare data to send when performing a synchronization. + * + * @param question Question. + * @param answers Answers of the question, without the prefix. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async prepareSyncData(question: any, answers: {[name: string]: any}, component: string, componentId: string | number, + siteId?: string): Promise { + + const element = this.domUtils.convertToElement(question.html); + const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]'); + + if (attachmentsInput) { + // Update the draft ID, the stored one could no longer be valid. + answers.attachments = attachmentsInput.value; + } + + if (answers && answers.attachments_offline) { + const attachmentsData = this.textUtils.parseJSON(answers.attachments_offline, {}); + + // Check if any attachment was deleted. + const originalAttachments = this.questionHelper.getResponseFileAreaFiles(question, 'attachments'); + const filesToDelete = CoreFileUploader.instance.getFilesToDelete(originalAttachments, attachmentsData.online); + + if (filesToDelete.length > 0) { + // Delete files. + await CoreFileUploader.instance.deleteDraftFiles(answers.attachments, filesToDelete, siteId); + } + + if (attachmentsData.offline) { + // Upload the offline files. + const offlineFiles = await this.questionHelper.getStoredQuestionFiles(question, component, componentId, siteId); + + await CoreFileUploader.instance.uploadFiles(answers.attachments, attachmentsData.online.concat(offlineFiles), + siteId); + } + + delete answers.attachments_offline; + } + } + + /** + * Prepare the text answer. + * + * @param question Question. + * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. + * @param textarea The textarea HTML element of the question. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async prepareTextAnswer(question: any, answers: any, textarea: HTMLTextAreaElement, siteId?: string): Promise { + if (this.questionHelper.hasDraftFileUrls(question.html) && question.responsefileareas) { + // Restore draftfile URLs. + const site = await CoreSites.instance.getSite(siteId); + + answers[textarea.name] = this.textUtils.restoreDraftfileUrls(site.getURL(), answers[textarea.name], + question.html, this.questionHelper.getResponseFileAreaFiles(question, 'answer')); + } + + let isPlainText = false; + if (question.isPlainText !== undefined) { + isPlainText = question.isPlainText; + } else if (question.settings) { + isPlainText = question.settings.responseformat == 'monospaced' || question.settings.responseformat == 'plain'; + } else { + const questionEl = this.domUtils.convertToElement(question.html); + isPlainText = !!questionEl.querySelector('.qtype_essay_monospaced') || !!questionEl.querySelector('.qtype_essay_plain'); + } + + if (!isPlainText) { // Add some HTML to the text if needed. answers[textarea.name] = this.textUtils.formatHtmlLines(answers[textarea.name]); } diff --git a/src/addon/qtype/gapselect/providers/handler.ts b/src/addon/qtype/gapselect/providers/handler.ts index 50b83068e..26ee5c10e 100644 --- a/src/addon/qtype/gapselect/providers/handler.ts +++ b/src/addon/qtype/gapselect/providers/handler.ts @@ -61,9 +61,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // We should always get a value for each select so we can assume we receive all the possible answers. for (const name in answers) { const value = answers[name]; @@ -90,9 +92,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // We should always get a value for each select so we can assume we receive all the possible answers. for (const name in answers) { const value = answers[name]; @@ -110,9 +114,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } } diff --git a/src/addon/qtype/match/providers/handler.ts b/src/addon/qtype/match/providers/handler.ts index 90d87d1d1..dcc287984 100644 --- a/src/addon/qtype/match/providers/handler.ts +++ b/src/addon/qtype/match/providers/handler.ts @@ -61,9 +61,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // We should always get a value for each select so we can assume we receive all the possible answers. for (const name in answers) { const value = answers[name]; @@ -90,9 +92,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // We should always get a value for each select so we can assume we receive all the possible answers. for (const name in answers) { const value = answers[name]; @@ -110,9 +114,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } } diff --git a/src/addon/qtype/multianswer/providers/handler.ts b/src/addon/qtype/multianswer/providers/handler.ts index 798dcf572..5cdf85a99 100644 --- a/src/addon/qtype/multianswer/providers/handler.ts +++ b/src/addon/qtype/multianswer/providers/handler.ts @@ -62,9 +62,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // Get all the inputs in the question to check if they've all been answered. const names = this.questionProvider.getBasicAnswers(this.questionHelper.getAllInputNamesFromHtml(question.html)); for (const name in names) { @@ -92,9 +94,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // We should always get a value for each select so we can assume we receive all the possible answers. for (const name in answers) { const value = answers[name]; @@ -112,9 +116,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } diff --git a/src/addon/qtype/multichoice/providers/handler.ts b/src/addon/qtype/multichoice/providers/handler.ts index 92424fbd2..1bd43f76b 100644 --- a/src/addon/qtype/multichoice/providers/handler.ts +++ b/src/addon/qtype/multichoice/providers/handler.ts @@ -45,9 +45,11 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { let isSingle = true, isMultiComplete = false; @@ -95,10 +97,12 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers); + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + return this.isCompleteResponse(question, answers, component, componentId); } /** @@ -118,9 +122,11 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { let isSingle = true, isMultiSame = true; @@ -158,10 +164,13 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string) + : void | Promise { if (question && !question.multi && typeof answers[question.optionsName] != 'undefined' && !answers[question.optionsName]) { /* It's a single choice and the user hasn't answered. Delete the answer because sending an empty string (default value) will mark the first option as selected. */ diff --git a/src/addon/qtype/randomsamatch/providers/handler.ts b/src/addon/qtype/randomsamatch/providers/handler.ts index a0eede940..8c48923f2 100644 --- a/src/addon/qtype/randomsamatch/providers/handler.ts +++ b/src/addon/qtype/randomsamatch/providers/handler.ts @@ -46,11 +46,13 @@ export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question behaves like a match question. - return this.matchHandler.isCompleteResponse(question, answers); + return this.matchHandler.isCompleteResponse(question, answers, component, componentId); } /** @@ -68,11 +70,13 @@ export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question behaves like a match question. - return this.matchHandler.isGradableResponse(question, answers); + return this.matchHandler.isGradableResponse(question, answers, component, componentId); } /** @@ -81,10 +85,12 @@ export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { // This question behaves like a match question. - return this.matchHandler.isSameResponse(question, prevAnswers, newAnswers); + return this.matchHandler.isSameResponse(question, prevAnswers, newAnswers, component, componentId); } } diff --git a/src/addon/qtype/shortanswer/providers/handler.ts b/src/addon/qtype/shortanswer/providers/handler.ts index f9629fa75..14abddb04 100644 --- a/src/addon/qtype/shortanswer/providers/handler.ts +++ b/src/addon/qtype/shortanswer/providers/handler.ts @@ -45,9 +45,11 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { return (answers['answer'] || answers['answer'] === 0) ? 1 : 0; } @@ -66,10 +68,12 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers); + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + return this.isCompleteResponse(question, answers, null, null); } /** @@ -78,9 +82,11 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); } } diff --git a/src/addon/qtype/truefalse/providers/handler.ts b/src/addon/qtype/truefalse/providers/handler.ts index f811dfa65..a8ef44aac 100644 --- a/src/addon/qtype/truefalse/providers/handler.ts +++ b/src/addon/qtype/truefalse/providers/handler.ts @@ -46,9 +46,11 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { return answers['answer'] ? 1 : 0; } @@ -67,10 +69,12 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers); + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + return this.isCompleteResponse(question, answers, null, null); } /** @@ -79,9 +83,11 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); } @@ -91,10 +97,13 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string) + : void | Promise { if (question && typeof answers[question.optionsName] != 'undefined' && !answers[question.optionsName]) { // The user hasn't answered. Delete the answer to prevent marking one of the answers automatically. delete answers[question.optionsName]; diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.html b/src/addon/storagemanager/pages/course-storage/course-storage.html index 88dffd229..b489def78 100644 --- a/src/addon/storagemanager/pages/course-storage/course-storage.html +++ b/src/addon/storagemanager/pages/course-storage/course-storage.html @@ -7,7 +7,10 @@ -

{{ course.displayname }}

+

+ {{ course.displayname }} + {{ course.fullname }} +

{{ 'addon.storagemanager.info' | translate }}

diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.ts b/src/addon/storagemanager/pages/course-storage/course-storage.ts index 6bdeecc52..7dace4b7b 100644 --- a/src/addon/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addon/storagemanager/pages/course-storage/course-storage.ts @@ -70,7 +70,7 @@ export class AddonStorageManagerCourseStoragePage { // But these aren't necessarily consistent, for example mod_frog vs mmaModFrog. // There is nothing enforcing correct values. // Most modules which have large files are downloadable, so I think this is sufficient. - const promise = this.prefetchDelegate.getModuleDownloadedSize(module, this.course.id). + const promise = this.prefetchDelegate.getModuleStoredSize(module, this.course.id). then((size) => { // There are some cases where the return from this is not a valid number. if (!isNaN(size)) { @@ -100,7 +100,7 @@ export class AddonStorageManagerCourseStoragePage { */ async deleteForCourse(): Promise { try { - await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await this.domUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -130,7 +130,7 @@ export class AddonStorageManagerCourseStoragePage { */ async deleteForSection(section: any): Promise { try { - await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await this.domUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -160,7 +160,7 @@ export class AddonStorageManagerCourseStoragePage { } try { - await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await this.domUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -184,8 +184,8 @@ export class AddonStorageManagerCourseStoragePage { const promises = []; modules.forEach((module) => { // Remove the files. - const promise = this.prefetchDelegate.removeModuleFiles(module, this.course.id).then(() => { - // When the files are removed, update the size. + const promise = this.courseHelperProvider.removeModuleStoredData(module, this.course.id).then(() => { + // When the files and cache are removed, update the size. module.parentSection.totalSize -= module.totalSize; this.totalSize -= module.totalSize; module.totalSize = 0; diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.html b/src/addon/storagemanager/pages/courses-storage/courses-storage.html index 937ddd2be..285510235 100644 --- a/src/addon/storagemanager/pages/courses-storage/courses-storage.html +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.html @@ -24,7 +24,10 @@ -

{{ course.displayname }}

+

+ {{ course.displayname }} + {{ course.fullname }} +

{{ 'core.downloading' | translate }}

diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.ts b/src/addon/storagemanager/pages/courses-storage/courses-storage.ts index 05b5aa709..5beb27b09 100644 --- a/src/addon/storagemanager/pages/courses-storage/courses-storage.ts +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.ts @@ -92,7 +92,7 @@ export class AddonStorageManagerCoursesStoragePage { */ async deleteCompletelyDownloadedCourses(): Promise { try { - await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -122,7 +122,7 @@ export class AddonStorageManagerCoursesStoragePage { */ async deleteCourse(course: DownloadedCourse): Promise { try { - await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -206,7 +206,7 @@ export class AddonStorageManagerCoursesStoragePage { const sections = await CoreCourse.instance.getSections(courseId); const modules = CoreArray.flatten(sections.map((section) => section.modules)); const promisedModuleSizes = modules.map(async (module) => { - const size = await CoreCourseModulePrefetch.instance.getModuleDownloadedSize(module, courseId); + const size = await CoreCourseModulePrefetch.instance.getModuleStoredSize(module, courseId); return isNaN(size) ? 0 : size; }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 434d6925b..1dc6627bb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Component, OnInit, NgZone } from '@angular/core'; -import { Platform, IonicApp } from 'ionic-angular'; +import { Config, Platform, IonicApp } from 'ionic-angular'; import { Network } from '@ionic-native/network'; -import { CoreAppProvider } from '@providers/app'; +import { CoreApp, CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLangProvider } from '@providers/lang'; import { CoreLoggerProvider } from '@providers/logger'; @@ -27,6 +27,8 @@ import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { Keyboard } from '@ionic-native/keyboard'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { CoreLoginSitesPage } from '@core/login/pages/sites/sites'; +import { CoreWindow } from '@singletons/window'; +import { Device } from '@ionic-native/device'; @Component({ templateUrl: 'app.html' @@ -39,13 +41,62 @@ export class MoodleMobileApp implements OnInit { protected lastUrls = {}; protected lastInAppUrl: string; - constructor(private platform: Platform, logger: CoreLoggerProvider, keyboard: Keyboard, private app: IonicApp, - private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider, private zone: NgZone, - private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider, - private screenOrientation: ScreenOrientation, private urlSchemesProvider: CoreCustomURLSchemesProvider, - private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private network: Network) { + constructor( + private platform: Platform, + logger: CoreLoggerProvider, + keyboard: Keyboard, + config: Config, + device: Device, + private app: IonicApp, + private eventsProvider: CoreEventsProvider, + private loginHelper: CoreLoginHelperProvider, + private zone: NgZone, + private appProvider: CoreAppProvider, + private langProvider: CoreLangProvider, + private sitesProvider: CoreSitesProvider, + private screenOrientation: ScreenOrientation, + private urlSchemesProvider: CoreCustomURLSchemesProvider, + private utils: CoreUtilsProvider, + private urlUtils: CoreUrlUtilsProvider, + private network: Network + ) { this.logger = logger.getInstance('AppComponent'); + if (this.appProvider.isIOS() && !platform.is('ios')) { + // Solve problem with wrong detected iPadOS. + const platforms = platform.platforms(); + const index = platforms.indexOf('core'); + if (index > -1) { + platforms.splice(index, 1); + } + platforms.push('mobile'); + platforms.push('ios'); + platforms.push('ipad'); + platforms.push('tablet'); + + app.setElementClass('app-root-ios', true); + platform.ready().then(() => { + if (device.version) { + const [major, minor]: string[] = device.version.split('.', 2); + app.setElementClass('platform-ios' + major, true); + app.setElementClass('platform-ios' + major + '_' + minor, true); + } + }); + + app._elementRef.nativeElement.classList.remove('app-root-md'); + + const iosConfig = config.getModeConfig('ios'); + + config.set('mode', 'ios'); + + Object.keys(iosConfig).forEach((key) => { + // Already overriden: pageTransition, do not change. + if (key != 'pageTransition') { + config.set('ios', key, iosConfig[key]); + } + }); + } + platform.ready().then(() => { // Okay, so the platform is ready and our plugins are available. // Here you can do any higher level native things you might need. @@ -145,7 +196,7 @@ export class MoodleMobileApp implements OnInit { }); this.utils.closeInAppBrowser(false); - } else if (this.platform.is('android')) { + } else if (CoreApp.instance.isAndroid()) { // Check if the URL has a custom URL scheme. In Android they need to be opened manually. const urlScheme = this.urlUtils.getUrlProtocol(url); if (urlScheme && urlScheme !== 'file' && urlScheme !== 'cdvfile') { @@ -207,6 +258,11 @@ export class MoodleMobileApp implements OnInit { }); }; + // "Expose" CoreWindow.open. + ( window).openWindowSafely = (url: string, name?: string, windowFeatures?: string): void => { + CoreWindow.open(url, name); + }; + // Load custom lang strings. This cannot be done inside the lang provider because it causes circular dependencies. const loadCustomStrings = (): void => { const currentSite = this.sitesProvider.getCurrentSite(), @@ -254,7 +310,7 @@ export class MoodleMobileApp implements OnInit { // Pause Youtube videos in Android when app is put in background or screen is locked. this.platform.pause.subscribe(() => { - if (!this.platform.is('android')) { + if (!CoreApp.instance.isAndroid()) { return; } @@ -279,7 +335,7 @@ export class MoodleMobileApp implements OnInit { // Detect orientation changes. this.screenOrientation.onChange().subscribe( () => { - if (this.platform.is('ios')) { + if (CoreApp.instance.isIOS()) { // Force ios to recalculate safe areas when rotating. // This can be erased when https://issues.apache.org/jira/browse/CB-13448 issue is solved or // After switching to WkWebview. diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index 76b7d379d..72ee007a5 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -120,4 +120,15 @@ ion-app.app-root.ios { cursor: pointer; } } + + video { + &::cue { + white-space: pre-line; + } + + &::-webkit-media-text-track-display-backdrop { + margin-left: 1.5%; + margin-right: 1.5%; + } + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d5080dccb..cb49b0c71 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -25,6 +25,8 @@ import { MockLocationStrategy } from '@angular/common/testing'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { Diagnostic } from '@ionic-native/diagnostic'; +import { Geolocation } from '@ionic-native/geolocation'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { MoodleMobileApp } from './app.component'; @@ -59,6 +61,7 @@ import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; import { CoreSyncProvider } from '@providers/sync'; import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; +import { CoreGeolocationProvider } from '@providers/geolocation'; // Handlers. import { CoreSiteInfoCronHandler } from '@providers/handlers/site-info-cron-handler'; @@ -194,7 +197,8 @@ export const CORE_PROVIDERS: any[] = [ CorePluginFileDelegate, CoreSyncProvider, CoreFileHelperProvider, - CoreCustomURLSchemesProvider + CoreCustomURLSchemesProvider, + CoreGeolocationProvider, ]; export const WP_PROVIDER: any = null; @@ -342,12 +346,15 @@ export const WP_PROVIDER: any = null; CoreSyncProvider, CoreFileHelperProvider, CoreCustomURLSchemesProvider, + CoreGeolocationProvider, CoreSiteInfoCronHandler, { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true, }, + Diagnostic, + Geolocation, ScreenOrientation, {provide: COMPILER_OPTIONS, useValue: {}, multi: true}, {provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS]}, diff --git a/src/app/app.scss b/src/app/app.scss index 05557bc56..186461c52 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -873,6 +873,10 @@ ion-app.app-root { height: 100% !important; } + .core-modal-force-on-top { + z-index: 100000 !important; + } + @media only screen and (min-height: 400px) and (min-width: 300px) { .core-modal-lateral { @include core-split-area-end(); diff --git a/src/assets/exttomime.json b/src/assets/exttomime.json index 2728d8b3d..7720dc36e 100644 --- a/src/assets/exttomime.json +++ b/src/assets/exttomime.json @@ -5,7 +5,8 @@ "3dml": {"type":"text/vnd.in3d.3dml"}, "3ds": {"type":"image/x-3ds"}, "3g2": {"type":"video/3gpp2"}, -"3gp": {"type":"video/quicktime","icon":"quicktime","string":"video","groups":["video"]}, +"3gp": {"type":"video/3gpp","icon":"quicktime","string":"video","groups":["video"]}, +"3gpp": {"type":"video/3gpp","icon":"quicktime","string":"video","groups":["video"]}, "7z": {"type":"application/x-7z-compressed","icon":"archive","string":"archive","groups":["archive"]}, "a": {"type":"application/octet-stream"}, "aab": {"type":"application/x-authorware-bin"}, diff --git a/src/assets/js/iframe-treat-links.js b/src/assets/js/iframe-treat-links.js index 68f83571b..c585c96fe 100644 --- a/src/assets/js/iframe-treat-links.js +++ b/src/assets/js/iframe-treat-links.js @@ -49,7 +49,7 @@ // Find the link being clicked. var el = event.target; - while (el && el.tagName !== 'A') { + while (el && (el.tagName !== 'A' && el.tagName !== 'a')) { el = el.parentElement; } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index ec064e2dd..8657003fb 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -45,7 +45,6 @@ "addon.block_myoverview.hiddencourses": "Removed from view", "addon.block_myoverview.inprogress": "In progress", "addon.block_myoverview.lastaccessed": "Last accessed", - "addon.block_myoverview.morecourses": "More courses", "addon.block_myoverview.nocourses": "No courses", "addon.block_myoverview.past": "Past", "addon.block_myoverview.pluginname": "Course overview", @@ -408,6 +407,7 @@ "addon.mod_assign.submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.", "addon.mod_assign.submittedearly": "Assignment was submitted {{$a}} early", "addon.mod_assign.submittedlate": "Assignment was submitted {{$a}} late", + "addon.mod_assign.syncblockedusercomponent": "user grade", "addon.mod_assign.timemodified": "Last modified", "addon.mod_assign.timeremaining": "Time remaining", "addon.mod_assign.ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", @@ -463,6 +463,7 @@ "addon.mod_choice.errorgetchoice": "Error getting choice data.", "addon.mod_choice.expired": "This activity closed on {{$a}}.", "addon.mod_choice.full": "(Full)", + "addon.mod_choice.limita": "Limit: {{$a}}", "addon.mod_choice.modulenameplural": "Choices", "addon.mod_choice.noresultsviewable": "The results are not currently viewable.", "addon.mod_choice.notopenyet": "This activity is not available until {{$a}}.", @@ -476,6 +477,7 @@ "addon.mod_choice.publishinfonever": "The results of this activity will not be published after you answer.", "addon.mod_choice.removemychoice": "Remove my choice", "addon.mod_choice.responses": "Responses", + "addon.mod_choice.responsesa": "Responses: {{$a}}", "addon.mod_choice.responsesresultgraphdescription": "{{number}}% of the users chose the option: {{text}}.", "addon.mod_choice.responsesresultgraphheader": "Graph display", "addon.mod_choice.resultsnotsynced": "Your last response must be synchronised before it is included in the results.", @@ -505,11 +507,13 @@ "addon.mod_data.foundrecords": "Found records: {{$a.num}}/{{$a.max}} (Reset filters)", "addon.mod_data.gettinglocation": "Getting location", "addon.mod_data.latlongboth": "Both latitude and longitude are required.", + "addon.mod_data.locationnotenabled": "Location is not enabled", "addon.mod_data.locationpermissiondenied": "Permission to access your location has been denied.", "addon.mod_data.menuchoose": "Choose...", "addon.mod_data.modulenameplural": "Databases", "addon.mod_data.more": "More", "addon.mod_data.mylocation": "My location", + "addon.mod_data.noaccess": "You do not have access to this page", "addon.mod_data.nomatch": "No matching entries found!", "addon.mod_data.norecords": "No entries in database", "addon.mod_data.notapproved": "Entry is not approved yet.", @@ -1370,6 +1374,8 @@ "core.cannotconnecttrouble": "We're having trouble connecting to your site.", "core.cannotconnectverify": "Please check the address is correct.", "core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", + "core.cannotopeninapp": "This file may not work as expected on this device. Would you like to open it anyway?", + "core.cannotopeninappdownload": "This file may not work as expected on this device. Would you like to download it anyway?", "core.captureaudio": "Record audio", "core.capturedimage": "Taken picture.", "core.captureimage": "Take picture", @@ -1378,6 +1384,7 @@ "core.choose": "Choose", "core.choosedots": "Choose...", "core.clearsearch": "Clear search", + "core.clearstoreddata": "Clear storage {{$a}}", "core.clicktohideshow": "Click to expand or collapse", "core.clicktoseefull": "Click to see full contents.", "core.close": "Close", @@ -1431,6 +1438,7 @@ "core.course.availablespace": " You currently have about {{available}} free space.", "core.course.cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", "core.course.confirmdeletemodulefiles": "Are you sure you want to delete these files?", + "core.course.confirmdeletestoreddata": "Are you sure you want to delete the stored data?", "core.course.confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", "core.course.confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?", "core.course.confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?", @@ -1522,6 +1530,7 @@ "core.done": "Done", "core.download": "Download", "core.downloaded": "Downloaded", + "core.downloadfile": "Download file", "core.downloading": "Downloading", "core.edit": "Edit", "core.editor.autosavesucceeded": "Draft saved.", @@ -1557,6 +1566,7 @@ "core.errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.", "core.errorsync": "An error occurred while synchronising. Please try again.", "core.errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.", + "core.errorurlschemeinvalidscheme": "This URL is meant to be used in another app: {{$a}}.", "core.errorurlschemeinvalidsite": "This site URL cannot be opened in this app.", "core.explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.", "core.favourites": "Starred", @@ -1750,6 +1760,7 @@ "core.login.erroraccesscontrolalloworigin": "The cross-origin call you're trying to perform has been rejected. Please check https://docs.moodle.org/dev/Moodle_Mobile_development_using_Chrome_or_Chromium", "core.login.errordeletesite": "An error occurred while deleting this site. Please try again.", "core.login.errorexampleurl": "The URL https://campus.example.edu is only an example URL, it's not a real site. Please use the URL of your school or organization's site.", + "core.login.errorqrnoscheme": "This URL isn't a valid login URL.", "core.login.errorupdatesite": "An error occurred while updating the site's token.", "core.login.faqcannotconnectanswer": "Please, contact your site administrator.", "core.login.faqcannotconnectquestion": "I typed my site address correctly but I still can't connect.", @@ -1826,7 +1837,9 @@ "core.login.selectacountry": "Select a country", "core.login.selectsite": "Please select your site:", "core.login.signupplugindisabled": "{{$a}} is not enabled.", + "core.login.signuprequiredfieldnotsupported": "The signup form contains a required custom field that isn't supported in the app. Please create your account using a web browser.", "core.login.siteaddress": "Your site", + "core.login.siteaddressplaceholder": "https://campus.example.edu", "core.login.sitehasredirect": "Your site contains at least one HTTP redirect. The app cannot follow redirects, this could be the issue that's preventing the app from connecting to your site.", "core.login.siteinmaintenance": "Your site is in maintenance mode", "core.login.sitepolicynotagreederror": "Site policy not agreed.", @@ -1848,6 +1861,7 @@ "core.mainmenu.help": "Help", "core.mainmenu.logout": "Log out", "core.mainmenu.website": "Website", + "core.maxfilesize": "Maximum size for new files: {{$a}}", "core.maxsizeandattachments": "Maximum file size: {{$a.size}}, maximum number of files: {{$a.attachments}}", "core.min": "min", "core.mins": "mins", @@ -1899,6 +1913,7 @@ "core.noresults": "No results", "core.noselection": "No selection", "core.notapplicable": "n/a", + "core.notavailable": "Not available", "core.notenrolledprofile": "This profile is not available because this user is not enrolled in this course.", "core.notice": "Notice", "core.notingroup": "Sorry, but you need to be part of a group to see this page.", @@ -1909,11 +1924,13 @@ "core.offline": "Offline", "core.ok": "OK", "core.online": "Online", + "core.openfile": "Open file", "core.openfullimage": "Click here to display the full size image", "core.openinbrowser": "Open in browser", "core.openmodinbrowser": "Open {{$a}} in browser", "core.othergroups": "Other groups", "core.pagea": "Page {{$a}}", + "core.parentlanguage": "", "core.paymentinstant": "Use the button below to pay and be enrolled within minutes!", "core.percentagenumber": "{{$a}}%", "core.phone": "Phone", @@ -1928,8 +1945,8 @@ "core.question.certainty": "Certainty", "core.question.complete": "Complete", "core.question.correct": "Correct", - "core.question.errorattachmentsnotsupported": "The application doesn't support attaching files to answers yet.", - "core.question.errorinlinefilesnotsupported": "The application doesn't support editing inline files yet.", + "core.question.errorattachmentsnotsupportedinsite": "Your site doesn't support attaching files to answers yet.", + "core.question.errorembeddedfilesnotsupportedinsite": "Your site doesn't support editing embedded files yet.", "core.question.errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.", "core.question.feedback": "Feedback", "core.question.howtodraganddrop": "Tap to select then tap to drop.", @@ -1980,10 +1997,12 @@ "core.settings.about": "About", "core.settings.appsettings": "App settings", "core.settings.appversion": "App version", + "core.settings.cannotsyncloggedout": "This site cannot be synchronised because you've logged out. Please try again when you're logged in the site again.", "core.settings.cannotsyncoffline": "Cannot synchronise offline.", "core.settings.cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", "core.settings.colorscheme": "Color Scheme", "core.settings.colorscheme-auto": "Auto (based on system settings)", + "core.settings.colorscheme-auto-notice": "Auto mode may not work in some Android devices.", "core.settings.colorscheme-dark": "Dark", "core.settings.colorscheme-light": "Light", "core.settings.compilationinfo": "Compilation info", diff --git a/src/assets/mimetoext.json b/src/assets/mimetoext.json index 438731f8b..6e8770637 100644 --- a/src/assets/mimetoext.json +++ b/src/assets/mimetoext.json @@ -1018,7 +1018,7 @@ "text/x-vcard": ["vcf"], "text/xml": ["resx","jcb","jcw","jmt","jmx","jcl","xsl","rhb","sqt","xml","jqz"], "text/yaml": ["yaml","yml"], -"video/3gpp": ["3gp"], +"video/3gpp": ["3gp", "3gpp"], "video/3gpp2": ["3g2"], "video/animaflex": ["afl"], "video/avi": ["avi"], diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index 14bea2fd7..81d7f8ed8 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -20,6 +20,18 @@ import { CoreAppProvider } from '@providers/app'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +/** + * Blocked sync error. + */ +export class CoreSyncBlockedError extends Error { + constructor(message: string) { + super(message); + + // Set the prototype explicitly, otherwise instanceof won't work as expected. + Object.setPrototypeOf(this, CoreSyncBlockedError.prototype); + } +} + /** * Base class to create sync providers. It provides some common functions. */ @@ -52,6 +64,26 @@ export class CoreSyncBaseProvider { this.component = component; } + /** + * Add an offline data deleted warning to a list of warnings. + * + * @param warnings List of warnings. + * @param component Component. + * @param name Instance name. + * @param error Specific error message. + */ + protected addOfflineDataDeletedWarning(warnings: string[], component: string, name: string, error: string): void { + const warning = this.translate.instant('core.warningofflinedatadeleted', { + component: component, + name: name, + error: error, + }); + + if (warnings.indexOf(warning) == -1) { + warnings.push(warning); + } + } + /** * Add an ongoing sync to the syncPromises list. On finish the promise will be removed. * @@ -60,7 +92,7 @@ export class CoreSyncBaseProvider { * @param siteId Site ID. If not defined, current site. * @return The sync promise. */ - addOngoingSync(id: string | number, promise: Promise, siteId?: string): Promise { + addOngoingSync(id: string | number, promise: Promise, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const uniqueId = this.getUniqueSyncId(id); diff --git a/src/classes/error.ts b/src/classes/error.ts new file mode 100644 index 000000000..2bbb0b737 --- /dev/null +++ b/src/classes/error.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Base Error class. + * + * The native Error class cannot be extended in Typescript without restoring the prototype chain, extend this + * class instead. + * + * @see https://stackoverflow.com/questions/41102060/typescript-extending-error-class + */ +export class CoreError extends Error { + + constructor(message?: string) { + super(message); + + // Fix prototype chain: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + this.name = new.target.name; + Object.setPrototypeOf(this, new.target.prototype); + } + +} diff --git a/src/classes/queue-runner.ts b/src/classes/queue-runner.ts new file mode 100644 index 000000000..88849abdd --- /dev/null +++ b/src/classes/queue-runner.ts @@ -0,0 +1,143 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreUtils, PromiseDefer } from '@providers/utils/utils'; + +/** + * Function to add to the queue. + */ +export type CoreQueueRunnerFunction = (...args: any[]) => T | Promise; + +/** + * Queue item. + */ +export type CoreQueueRunnerItem = { + /** + * Item ID. + */ + id: string; + + /** + * Function to execute. + */ + fn: CoreQueueRunnerFunction; + + /** + * Deferred with a promise resolved/rejected with the result of the function. + */ + deferred: PromiseDefer; +}; + +/** + * Options to pass to add item. + */ +export type CoreQueueRunnerAddOptions = { + /** + * Whether to allow having multiple entries with same ID in the queue. + */ + allowRepeated?: boolean; +}; + +/** + * A queue to prevent having too many concurrent executions. + */ +export class CoreQueueRunner { + protected queue: {[id: string]: CoreQueueRunnerItem} = {}; + protected orderedQueue: CoreQueueRunnerItem[] = []; + protected numberRunning = 0; + + constructor(protected maxParallel: number = 1) { } + + /** + * Get unique ID. + * + * @param id ID. + * @return Unique ID. + */ + protected getUniqueId(id: string): string { + let newId = id; + let num = 1; + + do { + newId = id + '-' + num; + num++; + } while (newId in this.queue); + + return newId; + } + + /** + * Process next item in the queue. + * + * @return Promise resolved when next item has been treated. + */ + protected async processNextItem(): Promise { + if (!this.orderedQueue.length || this.numberRunning >= this.maxParallel) { + // Queue is empty or max number of parallel runs reached, stop. + return; + } + + const item = this.orderedQueue.shift(); + this.numberRunning++; + + try { + const result = await item.fn(); + + item.deferred.resolve(result); + } catch (error) { + item.deferred.reject(error); + } finally { + delete this.queue[item.id]; + this.numberRunning--; + + this.processNextItem(); + } + } + + /** + * Add an item to the queue. + * + * @param id ID. + * @param fn Function to call. + * @param options Options. + * @return Promise resolved when the function has been executed. + */ + run(id: string, fn: CoreQueueRunnerFunction, options?: CoreQueueRunnerAddOptions): Promise { + options = options || {}; + + if (id in this.queue) { + if (!options.allowRepeated) { + // Item already in queue, return its promise. + return this.queue[id].deferred.promise; + } + + id = this.getUniqueId(id); + } + + // Add the item in the queue. + const item = { + id, + fn, + deferred: CoreUtils.instance.promiseDefer(), + }; + + this.queue[id] = item; + this.orderedQueue.push(item); + + // Process next item if we haven't reached the max yet. + this.processNextItem(); + + return item.deferred.promise; + } +} diff --git a/src/classes/site.ts b/src/classes/site.ts index 463b5a027..0502b505c 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -20,7 +20,9 @@ import { CoreDbProvider } from '@providers/db'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets } from '@providers/ws'; +import { + CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets, CoreWSPreSetsSplitRequest +} from '@providers/ws'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -127,6 +129,23 @@ export interface CoreSiteWSPreSets { * Defaults to CoreSite.FREQUENCY_USUALLY. */ updateFrequency?: number; + + /** + * Component name. Optionally included if this request is being made on behalf of a specific + * component (e.g. activity). + */ + component?: string; + + /** + * Component id. Optionally included when 'component' is set. + */ + componentId?: number; + + /** + * Whether to split a request if it has too many parameters. Sending too many parameters to the site + * can cause the request to fail (see PHP's max_input_vars). + */ + splitRequest?: CoreWSPreSetsSplitRequest; } /** @@ -197,18 +216,21 @@ export class CoreSite { protected wsProvider: CoreWSProvider; // Variables for the database. - static WS_CACHE_TABLE = 'wscache'; + static WS_CACHE_TABLE = 'wscache_2'; static CONFIG_TABLE = 'core_site_config'; // Versions of Moodle releases. protected MOODLE_RELEASES = { - 3.1: 2016052300, - 3.2: 2016120500, - 3.3: 2017051503, - 3.4: 2017111300, - 3.5: 2018051700, - 3.6: 2018120300, - 3.7: 2019052000 + '3.1': 2016052300, + '3.2': 2016120500, + '3.3': 2017051503, + '3.4': 2017111300, + '3.5': 2018051700, + '3.6': 2018120300, + '3.7': 2019052000, + '3.8': 2019111800, + '3.9': 2020061500, + '3.10': 2020092400, // @todo: Replace with the right value once 3.10 is released. }; static MINIMUM_MOODLE_VERSION = '3.1'; @@ -639,7 +661,8 @@ export class CoreSite { siteUrl: this.siteUrl, cleanUnicode: this.cleanUnicode, typeExpected: preSets.typeExpected, - responseExpected: preSets.responseExpected + responseExpected: preSets.responseExpected, + splitRequest: preSets.splitRequest, }; if (wsPreSets.cleanUnicode && this.textUtils.hasUnicodeData(data)) { @@ -1089,6 +1112,25 @@ export class CoreSite { }); } + /** + * Gets the size of cached data for a specific component or component instance. + * + * @param component Component name + * @param componentId Optional component id (if not included, returns sum for whole component) + * @return Promise resolved when we have calculated the size + */ + getComponentCacheSize(component: string, componentId?: number): Promise { + const params: any[] = [component]; + let extraClause = ''; + if (componentId !== undefined && componentId !== null) { + params.push(componentId); + extraClause = ' AND componentId = ?'; + } + + return this.db.getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE + + ' WHERE component = ?' + extraClause, params); + } + /** * Save a WS response to cache. * @@ -1128,6 +1170,13 @@ export class CoreSite { entry.key = preSets.cacheKey; } + if (preSets.component) { + entry.component = preSets.component; + if (preSets.componentId) { + entry.componentId = preSets.componentId; + } + } + return this.db.insertRecord(CoreSite.WS_CACHE_TABLE, entry); }); } @@ -1155,6 +1204,33 @@ export class CoreSite { return this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { id: id }); } + /** + * Deletes WS cache entries for all methods relating to a specific component (and + * optionally component id). + * + * @param component Component name. + * @param componentId Component id. + * @return Promise resolved when the entries are deleted. + */ + async deleteComponentFromCache(component: string, componentId?: number): Promise { + if (!component) { + return; + } + + if (!this.db) { + throw new Error('Site DB not initialized'); + } + + const params = { + component: component + } as any; + if (componentId) { + params.componentId = componentId; + } + + return this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, params); + } + /* * Uploads a file using Cordova File API. * @@ -1324,6 +1400,29 @@ export class CoreSite { } } + /** + * Gets an approximation of the cache table usage of the site. + * + * Currently this is just the total length of the data fields in the cache table. + * + * @return Promise resolved with the total size of all data in the cache table (bytes) + */ + getCacheUsage(): Promise { + return this.db.getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE); + } + + /** + * Gets a total of the file and cache usage. + * + * @return Promise with the total of getSpaceUsage and getCacheUsage + */ + async getTotalUsage(): Promise { + const space = await this.getSpaceUsage(); + const cache = await this.getCacheUsage(); + + return space + cache; + } + /** * Returns the URL to the documentation of the app, based on Moodle version and current language. * diff --git a/src/components/attachments/attachments.ts b/src/components/attachments/attachments.ts index 810165c39..b312f8d0f 100644 --- a/src/components/attachments/attachments.ts +++ b/src/components/attachments/attachments.ts @@ -15,6 +15,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; +import { CoreSites } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; @@ -39,8 +40,8 @@ import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/hel }) export class CoreAttachmentsComponent implements OnInit { @Input() files: any[]; // List of attachments. New attachments will be added to this array. - @Input() maxSize: number; // Max size for attachments. If not defined, 0 or -1, unknown size. - @Input() maxSubmissions: number; // Max number of attachments. If -1 or not defined, unknown limit. + @Input() maxSize: number; // Max size for attachments. -1 means unlimited, 0 means user max size, not defined means unknown. + @Input() maxSubmissions: number; // Max number of attachments. -1 means unlimited, not defined means unknown limit. @Input() component: string; // Component the downloaded files will be linked to. @Input() componentId: string | number; // Component ID. @Input() allowOffline: boolean | string; // Whether to allow selecting files in offline. @@ -61,17 +62,28 @@ export class CoreAttachmentsComponent implements OnInit { * Component being initialized. */ ngOnInit(): void { - this.maxSize = Number(this.maxSize); // Make sure it's defined and it's a number. - this.maxSize = !isNaN(this.maxSize) && this.maxSize > 0 ? this.maxSize : -1; + this.maxSize = this.maxSize !== null ? Number(this.maxSize) : NaN; - if (this.maxSize == -1) { - this.maxSizeReadable = this.translate.instant('core.unknown'); - } else { + if (this.maxSize === 0) { + const currentSite = CoreSites.instance.getCurrentSite(); + const siteInfo = currentSite && currentSite.getInfo(); + + if (siteInfo && siteInfo.usermaxuploadfilesize) { + this.maxSize = siteInfo.usermaxuploadfilesize; + this.maxSizeReadable = this.textUtils.bytesToSize(this.maxSize, 2); + } else { + this.maxSizeReadable = this.translate.instant('core.unknown'); + } + } else if (this.maxSize > 0) { this.maxSizeReadable = this.textUtils.bytesToSize(this.maxSize, 2); + } else if (this.maxSize === -1) { + this.maxSizeReadable = this.translate.instant('core.unlimited'); + } else { + this.maxSizeReadable = this.translate.instant('core.unknown'); } if (typeof this.maxSubmissions == 'undefined' || this.maxSubmissions < 0) { - this.maxSubmissionsReadable = this.translate.instant('core.unknown'); + this.maxSubmissionsReadable = this.maxSubmissions < 0 ? undefined : this.translate.instant('core.unknown'); this.unlimitedFiles = true; } else { this.maxSubmissionsReadable = String(this.maxSubmissions); diff --git a/src/components/attachments/core-attachments.html b/src/components/attachments/core-attachments.html index 7a1abcda8..f57780bad 100644 --- a/src/components/attachments/core-attachments.html +++ b/src/components/attachments/core-attachments.html @@ -1,5 +1,6 @@ - {{ 'core.maxsizeandattachments' | translate:{$a: {size: maxSizeReadable, attachments: maxSubmissionsReadable} } }} + {{ 'core.maxsizeandattachments' | translate:{$a: {size: maxSizeReadable, attachments: maxSubmissionsReadable} } }} + {{ 'core.maxfilesize' | translate:{$a: maxSizeReadable} }} diff --git a/src/components/file/file.ts b/src/components/file/file.ts index 5df003e25..34c89be6a 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -148,7 +148,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { * @param e Click event. * @param openAfterDownload Whether the file should be opened after download. */ - download(e?: Event, openAfterDownload: boolean = false): void { + async download(e?: Event, openAfterDownload: boolean = false): Promise { e && e.preventDefault(); e && e.stopPropagation(); @@ -181,32 +181,45 @@ export class CoreFileComponent implements OnInit, OnDestroy { if (openAfterDownload) { // File needs to be opened now. - this.openFile().catch((error) => { + try { + await this.openFile(); + } catch (error) { this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); - }); + } } else { - // File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big. - this.pluginFileDelegate.getFileSize({fileurl: this.fileUrl, filesize: this.fileSize}, this.siteId).then((size) => { + // File doesn't need to be opened (it's a prefetch). + if (!this.fileHelper.isOpenableInApp(this.file)) { + try { + await this.fileHelper.showConfirmOpenUnsupportedFile(true); + } catch (error) { + return; // Cancelled, stop. + } + } - const promise = size ? this.domUtils.confirmDownloadSize({ size: size, total: true }) : Promise.resolve(); + try { + // Show confirm modal if file size is defined and it's big. + const size = await this.pluginFileDelegate.getFileSize({fileurl: this.fileUrl, filesize: this.fileSize}, + this.siteId); - return promise.then(() => { - // User confirmed, add the file to queue. - return this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl).finally(() => { - this.isDownloading = true; + if (size) { + await this.domUtils.confirmDownloadSize({ size: size, total: true }); + } - this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component, - this.componentId, this.timemodified, undefined, undefined, 0, this.file).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); - this.calculateState(); - }); - }); - }).catch(() => { - // User cancelled. - }); - }).catch((error) => { + // User confirmed, add the file to queue. + await this.utils.ignoreErrors(this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl)); + + this.isDownloading = true; + + try { + await this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component, + this.componentId, this.timemodified, undefined, undefined, 0, this.file); + } catch (error) { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + this.calculateState(); + } + } catch (error) { this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); - }); + } } } diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index 14dc98c54..e1c8dd430 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -16,7 +16,7 @@ import { Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -import { NavController, Platform } from 'ionic-angular'; +import { NavController } from 'ionic-angular'; import { CoreFile } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -24,8 +24,6 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreIframeUtilsProvider } from '@providers/utils/iframe'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { CoreUrl } from '@singletons/url'; -import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies'; @Component({ selector: 'core-iframe', @@ -54,7 +52,7 @@ export class CoreIframeComponent implements OnChanges { protected urlUtils: CoreUrlUtilsProvider, protected utils: CoreUtilsProvider, @Optional() protected svComponent: CoreSplitViewComponent, - protected platform: Platform) { + ) { this.logger = logger.getInstance('CoreIframe'); this.loaded = new EventEmitter(); @@ -106,24 +104,7 @@ export class CoreIframeComponent implements OnChanges { if (changes.src) { const url = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue; - if (this.platform.is('ios') && url && !this.urlUtils.isLocalFileUrl(url)) { - // Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView. - try { - const win = window; - const urlParts = CoreUrl.parse(url); - - if (urlParts.domain) { - await win.WKWebViewCookies.setCookie({ - name: 'MoodleAppCookieForWKWebView', - value: '1', - domain: urlParts.domain, - }); - } - } catch (err) { - // Ignore errors. - this.logger.error('Error setting cookie', err); - } - } + await this.iframeUtils.fixIframeCookies(url); this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(CoreFile.instance.convertFileSrc(url)); diff --git a/src/components/local-file/local-file.ts b/src/components/local-file/local-file.ts index cd0c573a3..eba54e903 100644 --- a/src/components/local-file/local-file.ts +++ b/src/components/local-file/local-file.ts @@ -15,6 +15,7 @@ import { Component, Input, Output, OnInit, EventEmitter, ViewChild, ElementRef } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; +import { CoreFileHelper } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; @@ -103,7 +104,7 @@ export class CoreLocalFileComponent implements OnInit { * * @param e Click event. */ - fileClicked(e: Event): void { + async fileClicked(e: Event): Promise { if (this.editMode) { return; } @@ -114,6 +115,14 @@ export class CoreLocalFileComponent implements OnInit { if (this.utils.isTrueOrOne(this.overrideClick) && this.onClick.observers.length) { this.onClick.emit(); } else { + if (!CoreFileHelper.instance.isOpenableInApp(this.file)) { + try { + await CoreFileHelper.instance.showConfirmOpenUnsupportedFile(); + } catch (error) { + return; // Cancelled, stop. + } + } + this.utils.openFile(this.file.toURL()); } } diff --git a/src/components/show-password/show-password.ts b/src/components/show-password/show-password.ts index 6ccf84a05..3449268b0 100644 --- a/src/components/show-password/show-password.ts +++ b/src/components/show-password/show-password.ts @@ -15,7 +15,7 @@ import { Component, OnInit, AfterViewInit, Input, ElementRef } from '@angular/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { Platform } from 'ionic-angular'; +import { CoreApp } from '@providers/app'; /** * Component to allow showing and hiding a password. The affected input MUST have a name to identify it. @@ -47,8 +47,11 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit { protected input: HTMLInputElement; // Input affected. protected element: HTMLElement; // Current element. - constructor(element: ElementRef, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider, - private platform: Platform) { + constructor( + element: ElementRef, + private utils: CoreUtilsProvider, + private domUtils: CoreDomUtilsProvider + ) { this.element = element.nativeElement; } @@ -114,7 +117,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit { this.shown = !this.shown; this.setData(); - if (isFocused && this.platform.is('android')) { + if (isFocused && CoreApp.instance.isAndroid()) { // In Android, the keyboard is closed when the input type changes. Focus it again. setTimeout(() => { this.domUtils.focusElement(this.input); diff --git a/src/components/split-view/split-view.ts b/src/components/split-view/split-view.ts index 11f6e4dbd..58d605b64 100644 --- a/src/components/split-view/split-view.ts +++ b/src/components/split-view/split-view.ts @@ -49,18 +49,20 @@ export class CoreSplitViewComponent implements OnInit, OnDestroy { @ViewChild('menu') menu: Menu; @Input() when?: string | boolean = 'md'; + protected VIEW_EVENTS = ['willEnter', 'didEnter', 'willLeave', 'willLeave']; + protected isEnabled; protected masterPageName = ''; protected masterPageIndex = 0; protected loadDetailPage: any = false; protected element: HTMLElement; // Current element. - protected detailsDidEnterSubscription: Subscription; protected masterCanLeaveOverridden = false; protected originalMasterCanLeave: Function; protected ignoreSplitChanged = false; protected audioCaptureSubscription: Subscription; protected languageChangedSubscription: Subscription; protected pushOngoing: boolean; + protected viewEventsSubscriptions: Subscription[] = []; // Empty placeholder for the 'detail' page. detailPage: any = null; @@ -92,7 +94,7 @@ export class CoreSplitViewComponent implements OnInit, OnDestroy { this.masterPageIndex = this.masterNav.indexOf(this.masterNav.getActive()); this.emptyDetails(); - this.handleCanLeave(); + this.handleViewEvents(); } /** @@ -123,7 +125,7 @@ export class CoreSplitViewComponent implements OnInit, OnDestroy { */ handleCanLeave(): void { // Listen for the didEnter event on the details nav to detect everytime a page is loaded. - this.detailsDidEnterSubscription = this.detailNav.viewDidEnter.subscribe((detailsViewController: ViewController) => { + this.viewEventsSubscriptions.push(this.detailNav.viewDidEnter.subscribe((detailsViewController: ViewController) => { if (!this.isOn()) { return; } @@ -166,7 +168,30 @@ export class CoreSplitViewComponent implements OnInit, OnDestroy { }); }; } - }); + })); + } + + /** + * Handle Ionic Views lifecycle events in the details page. + */ + handleViewEvents(): void { + // Handle affected view events except ionViewCanLeave, propagating them to the details view. + const masterActiveView = this.masterNav.getActive(); + + for (const i in this.VIEW_EVENTS) { + const viewEvent = this.VIEW_EVENTS[i]; + + this.viewEventsSubscriptions.push(masterActiveView[viewEvent].subscribe(() => { + if (!this.isOn()) { + return; + } + + const activeView = this.detailNav.getActive(); + activeView && activeView[`_${viewEvent}`](); + })); + } + + this.handleCanLeave(); } /** @@ -274,8 +299,10 @@ export class CoreSplitViewComponent implements OnInit, OnDestroy { * Component being destroyed. */ ngOnDestroy(): void { - this.detailsDidEnterSubscription && this.detailsDidEnterSubscription.unsubscribe(); this.audioCaptureSubscription.unsubscribe(); this.languageChangedSubscription.unsubscribe(); + for (const i in this.viewEventsSubscriptions) { + this.viewEventsSubscriptions[i].unsubscribe(); + } } } diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 65cdb3bf2..3afb3e95b 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -52,6 +52,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe // Minimum tab's width to display fully the word "Competencies" which is the longest tab in the app. static MIN_TAB_WIDTH = 107; + // Max height that allows tab hiding. + static MAX_HEIGHT_TO_HIDE_TABS = 768; @Input() selectedIndex = 0; // Index of the tab to select. @Input() hideUntil = true; // Determine when should the contents be shown. @@ -130,7 +132,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe this.initializeTabs(); } - this.resizeFunction = this.calculateSlides.bind(this); + this.resizeFunction = this.windowResized.bind(this); window.addEventListener('resize', this.resizeFunction); } @@ -229,11 +231,24 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe * Calculate slides. */ calculateSlides(): void { - if (!this.isCurrentView || !this.tabsShown || !this.initialized) { + if (!this.isCurrentView || !this.initialized) { // Don't calculate if component isn't in current view, the calculations are wrong. return; } + if (!this.tabsShown) { + if (window.innerHeight >= CoreTabsComponent.MAX_HEIGHT_TO_HIDE_TABS) { + // Ensure tabbar is shown. + this.tabsShown = true; + this.tabBarElement.classList.remove('tabs-hidden'); + this.lastScroll = 0; + this.calculateTabBarHeight(); + } else { + // Don't recalculate. + return; + } + } + this.calculateMaxSlides().then(() => { this.updateSlides(); }); @@ -477,6 +492,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe * @param scrollElement Scroll element to check scroll position. */ showHideTabs(scrollElement: any): void { + // Always show on very tall screens. + if (window.innerHeight >= CoreTabsComponent.MAX_HEIGHT_TO_HIDE_TABS) { + return; + } + if (!this.tabBarHeight && this.topTabsElement.offsetHeight != this.tabBarHeight) { // Wrong tab height, recalculate it. this.calculateTabBarHeight(); @@ -488,7 +508,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe } const scroll = parseInt(scrollElement.scrollTop, 10); - if (scroll == 0) { + if (scroll <= 0) { // Ensure tabbar is shown. this.topTabsElement.style.transform = ''; this.originalTabsContainer.style.transform = ''; @@ -612,6 +632,15 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe }); } + /** + * Adapt tabs to a window resize. + */ + protected windowResized(): void { + setTimeout(() => { + this.calculateSlides(); + }); + } + /** * Component destroyed. */ diff --git a/src/components/user-avatar/user-avatar.ts b/src/components/user-avatar/user-avatar.ts index d537765df..56f41e5ea 100644 --- a/src/components/user-avatar/user-avatar.ts +++ b/src/components/user-avatar/user-avatar.ts @@ -87,7 +87,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { */ protected setFields(): void { const profileUrl = this.profileUrl || (this.user && (this.user.profileimageurl || this.user.userprofileimageurl || - this.user.userpictureurl || this.user.profileimageurlsmall)); + this.user.userpictureurl || this.user.profileimageurlsmall || (this.user.urls && this.user.urls.profileimage))); if (typeof profileUrl == 'string') { this.avatarUrl = profileUrl; diff --git a/src/config.json b/src/config.json index 3acd3205c..652848597 100644 --- a/src/config.json +++ b/src/config.json @@ -2,8 +2,8 @@ "app_id": "com.moodle.moodlemobile", "appname": "Moodle Mobile", "desktopappname": "Moodle Desktop", - "versioncode": 3920, - "versionname": "3.9.2", + "versioncode": 3930, + "versionname": "3.9.3", "cache_update_frequency_usually": 420000, "cache_update_frequency_often": 1200000, "cache_update_frequency_sometimes": 3600000, @@ -84,6 +84,7 @@ "siteurl": "", "sitename": "", "multisitesdisplay": "", + "sitefindersettings": {}, "onlyallowlistedsites": false, "skipssoconfirmation": false, "forcedefaultlanguage": false, @@ -100,6 +101,7 @@ "enableanalytics": false, "enableonboarding": true, "forceColorScheme": "", + "forceLoginLogo": false, "ioswebviewscheme": "moodleappfs", "appstores": { "android": "com.moodle.moodlemobile", diff --git a/src/core/block/components/course-blocks/core-block-course-blocks.html b/src/core/block/components/course-blocks/core-block-course-blocks.html index 47494de17..52b424c11 100644 --- a/src/core/block/components/course-blocks/core-block-course-blocks.html +++ b/src/core/block/components/course-blocks/core-block-course-blocks.html @@ -2,7 +2,7 @@

-
+
diff --git a/src/core/block/components/course-blocks/course-blocks.ts b/src/core/block/components/course-blocks/course-blocks.ts index a95a77178..69ca7adc9 100644 --- a/src/core/block/components/course-blocks/course-blocks.ts +++ b/src/core/block/components/course-blocks/course-blocks.ts @@ -30,6 +30,7 @@ export class CoreBlockCourseBlocksComponent implements OnInit { @Input() courseId: number; @Input() hideBlocks = false; + @Input() hideBottomBlocks = false; @Input() downloadEnabled: boolean; @ViewChildren(CoreBlockComponent) blocksComponents: QueryList; @@ -49,6 +50,7 @@ export class CoreBlockCourseBlocksComponent implements OnInit { * Component being initialized. */ ngOnInit(): void { + this.element.classList.add('core-no-blocks'); this.loadContent().finally(() => { this.dataLoaded = true; }); @@ -89,7 +91,7 @@ export class CoreBlockCourseBlocksComponent implements OnInit { this.blocks = []; }).finally(() => { - if (this.blocks.length > 0) { + if (!this.hideBlocks && this.blocks.length > 0) { this.element.classList.add('core-has-blocks'); this.element.classList.remove('core-no-blocks'); diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 85d063b14..9606c86d1 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable, Injector, Component, NgModule, Compiler, ComponentFactory, ComponentRef, NgModuleRef } from '@angular/core'; +import { + Injectable, Injector, Component, NgModule, Compiler, ComponentFactory, ComponentRef, NgModuleRef, NO_ERRORS_SCHEMA +} from '@angular/core'; import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; import { Platform, ActionSheetController, AlertController, LoadingController, ModalController, PopoverController, ToastController, @@ -57,7 +59,9 @@ import { Md5 } from 'ts-md5/dist/md5'; // Import core classes that can be useful for site plugins. import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreArray } from '@singletons/array'; import { CoreUrl } from '@singletons/url'; +import { CoreWindow } from '@singletons/window'; import { CoreCache } from '@classes/cache'; import { CoreDelegate } from '@classes/delegate'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; @@ -65,6 +69,7 @@ import { CoreContentLinksModuleGradeHandler } from '@core/contentlinks/classes/m import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler'; +import { CoreGeolocationError, CoreGeolocationErrorReason } from '@providers/geolocation'; // Import all core modules that define components, directives and pipes. import { CoreComponentsModule } from '@components/components.module'; @@ -177,7 +182,7 @@ export class CoreCompileProvider { const imports = this.IMPORTS.concat(extraImports); // Now create the module containing the component. - const module = NgModule({imports: imports, declarations: [component]})(class {}); + const module = NgModule({imports: imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA]})(class {}); try { // Compile the module and the component. @@ -191,6 +196,9 @@ export class CoreCompileProvider { } }); } catch (ex) { + this.logger.error('Error compiling template', template); + this.logger.error(ex); + return Promise.reject({message: 'Template has some errors and cannot be displayed.', debuginfo: ex}); } } @@ -270,7 +278,9 @@ export class CoreCompileProvider { instance['moment'] = moment; instance['Md5'] = Md5; instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; + instance['CoreArray'] = CoreArray; instance['CoreUrl'] = CoreUrl; + instance['CoreWindow'] = CoreWindow; instance['CoreCache'] = CoreCache; instance['CoreDelegate'] = CoreDelegate; instance['CoreContentLinksHandlerBase'] = CoreContentLinksHandlerBase; @@ -290,6 +300,8 @@ export class CoreCompileProvider { instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; instance['CoreSitePluginsAssignFeedbackComponent'] = CoreSitePluginsAssignFeedbackComponent; instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent; + instance['CoreGeolocationError'] = CoreGeolocationError; + instance['CoreGeolocationErrorReason'] = CoreGeolocationErrorReason; } /** diff --git a/src/core/contentlinks/providers/delegate.ts b/src/core/contentlinks/providers/delegate.ts index b323f60be..eb9c526e1 100644 --- a/src/core/contentlinks/providers/delegate.ts +++ b/src/core/contentlinks/providers/delegate.ts @@ -191,7 +191,7 @@ export class CoreContentLinksDelegate { // Add them to the list. linkActions.push({ - priority: handler.priority, + priority: handler.priority || 0, actions: actions }); } diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index 453928f72..43eae57e5 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -5,13 +5,13 @@ - + -
+
+ +
@@ -37,7 +46,7 @@ - + + +
- - - - - + - - - {{ 'core.login.selectsite' | translate }} - - {{site.name}} - - - - + - +

{{ 'core.login.selectsite' | translate }}

- -

{{site.name}}

-

{{site.url}}

+ + + + + +

{{site.title}}

+

{{site.noProtocolUrl}}

+

{{site.location}}

{{ 'core.login.selectsite' | translate }}

- {{site.name}} + {{site.title}}
@@ -99,8 +94,8 @@ - - {{ 'core.needhelp' | translate }} diff --git a/src/core/login/pages/site/site.scss b/src/core/login/pages/site/site.scss index bde235732..adf46e97a 100644 --- a/src/core/login/pages/site/site.scss +++ b/src/core/login/pages/site/site.scss @@ -12,7 +12,8 @@ ion-app.app-root page-core-login-site { } .item.item-block { - &.core-login-need-help.item { + &.core-login-need-help .item { + color: $core-login-page-text-color; text-decoration: underline; } &.core-login-site-qrcode { @@ -97,6 +98,19 @@ ion-app.app-root page-core-login-site { text-align: center; } + .core-login-site-list-loading, + .core-login-site-nolist-loading { + .spinner circle, .spinner line { + stroke: $core-login-loading-color; + } + + @include darkmode() { + .spinner circle, .spinner line { + stroke: $core-dark-login-loading-color; + } + } + } + .item.core-login-site-list-title { ion-label, ion-label h2.item-heading { margin-top: 0; @@ -123,6 +137,10 @@ ion-app.app-root page-core-login-site { ion-thumbnail { box-shadow: 0 0 4px #ddd; } + color: $core-login-page-text-color; + @include darkmode() { + color: $core-dark-login-page-text-color; + } } diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index e9d43091b..90248ad35 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -35,7 +35,17 @@ import { TranslateService } from '@ngx-translate/core'; */ type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & { noProtocolUrl?: string; // Url wihtout protocol. - country?: string; // Based on countrycode. + location?: string; // City + country. + title?: string; // Name + alias. +}; + +type SiteFinderSettings = { + displayalias: boolean, + displaycity: boolean, + displaycountry: boolean, + displayimage: boolean, + displaysitename: boolean, + displayurl: boolean }; /** @@ -51,8 +61,8 @@ export class CoreLoginSitePage { @ViewChild('siteFormEl') formElement: ElementRef; siteForm: FormGroup; - fixedSites: CoreLoginSiteInfo[]; - filteredSites: CoreLoginSiteInfo[]; + fixedSites: CoreLoginSiteInfoExtended[]; + filteredSites: CoreLoginSiteInfoExtended[]; siteSelector = 'sitefinder'; showKeyboard = false; filter = ''; @@ -62,6 +72,7 @@ export class CoreLoginSitePage { searchFnc: Function; showScanQR: boolean; enteredSiteUrl: CoreLoginSiteInfoExtended; + siteFinderSettings: SiteFinderSettings; constructor(navParams: NavParams, protected navCtrl: NavController, @@ -84,13 +95,37 @@ export class CoreLoginSitePage { let url = ''; this.siteSelector = CoreConfigConstants.multisitesdisplay; + const siteFinderSettings: Partial = CoreConfigConstants['sitefindersettings'] || {}; + this.siteFinderSettings = { + displaysitename: true, + displayimage: true, + displayalias: true, + displaycity: true, + displaycountry: true, + displayurl: true, + ...siteFinderSettings + }; + // Load fixed sites if they're set. if (this.loginHelper.hasSeveralFixedSites()) { - this.fixedSites = this.loginHelper.getFixedSites(); - // Autoselect if not defined. - if (['list', 'listnourl', 'select', 'buttons'].indexOf(this.siteSelector) < 0) { - this.siteSelector = this.fixedSites.length > 8 ? 'list' : (this.fixedSites.length > 3 ? 'select' : 'buttons'); + // Deprecate listnourl on 3.9.3, remove this block on the following release. + if (this.siteSelector == 'listnourl') { + this.siteSelector = 'list'; + this.siteFinderSettings.displayurl = false; } + + this.fixedSites = this.extendCoreLoginSiteInfo( this.loginHelper.getFixedSites()); + + // Do not show images if none are set. + if (!this.fixedSites.some((site) => !!site.imageurl)) { + this.siteFinderSettings.displayimage = false; + } + + // Autoselect if not defined. + if (this.siteSelector != 'list' && this.siteSelector != 'buttons') { + this.siteSelector = this.fixedSites.length > 3 ? 'list' : 'buttons'; + } + this.filteredSites = this.fixedSites; url = this.fixedSites[0].url; } else if (CoreConfigConstants.enableonboarding && !this.appProvider.isIOS() && !this.appProvider.isMac()) { @@ -116,11 +151,8 @@ export class CoreLoginSitePage { // Update the sites list. this.sites = await this.sitesProvider.findSites(search); - // UI tweaks. - this.sites.forEach((site) => { - site.noProtocolUrl = CoreUrl.removeProtocol(site.url); - site.country = this.utils.getCountryName(site.countrycode); - }); + // Add UI tweaks. + this.sites = this.extendCoreLoginSiteInfo(this.sites); this.hasSites = !!this.sites.length; } else { @@ -132,6 +164,34 @@ export class CoreLoginSitePage { }, 1000); } + /** + * Extend info of Login Site Info to get UI tweaks. + * + * @param sites Sites list. + * @return Sites list with extended info. + */ + protected extendCoreLoginSiteInfo(sites: CoreLoginSiteInfoExtended[]): CoreLoginSiteInfoExtended[] { + return sites.map((site) => { + site.noProtocolUrl = this.siteFinderSettings.displayurl && site.url ? CoreUrl.removeProtocol(site.url) : ''; + + const name = this.siteFinderSettings.displaysitename ? site.name : ''; + const alias = this.siteFinderSettings.displayalias && site.alias ? site.alias : ''; + + // Set title with parenthesis if both name and alias are present. + site.title = name && alias ? name + ' (' + alias + ')' : name + alias; + + const country = this.siteFinderSettings.displaycountry && site.countrycode ? + this.utils.getCountryName(site.countrycode) : ''; + const city = this.siteFinderSettings.displaycity && site.city ? + site.city : ''; + + // Separate location with hiphen if both country and city are present. + site.location = city && country ? city + ' - ' + country : city + country; + + return site; + }); + } + /** * Try to connect to a site. * @@ -224,7 +284,8 @@ export class CoreLoginSitePage { this.filteredSites = this.fixedSites; } else { this.filteredSites = this.fixedSites.filter((site) => { - return site.name.toLowerCase().indexOf(newValue) > -1 || site.url.toLowerCase().indexOf(newValue) > -1; + return site.title.toLowerCase().indexOf(newValue) > -1 || site.noProtocolUrl.toLowerCase().indexOf(newValue) > -1 || + site.location.toLowerCase().indexOf(newValue) > -1; }); } } @@ -335,7 +396,7 @@ export class CoreLoginSitePage { * @return Promise resolved after logging in. */ protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise { - return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => { + return this.sitesProvider.checkApplication(response.config).then(() => { this.domUtils.triggerFormSubmittedEvent(this.formElement, true); @@ -429,10 +490,19 @@ export class CoreLoginSitePage { } } } else { - // Not a custom URL scheme, put the text in the field. - this.siteForm.controls.siteUrl.setValue(text); + // Not a custom URL scheme, check if it's a URL scheme to another app. + const scheme = this.urlUtils.getUrlProtocol(text); - this.connect(new Event('click'), text); + if (scheme && scheme != 'http' && scheme != 'https') { + this.domUtils.showErrorModal(this.translate.instant('core.errorurlschemeinvalidscheme', {$a: text})); + } else if (this.loginHelper.isSiteUrlAllowed(text)) { + // Put the text in the field (if present). + this.siteForm.controls.siteUrl.setValue(text); + + this.connect(new Event('click'), text); + } else { + this.domUtils.showErrorModal('core.errorurlschemeinvalidsite', true); + } } } } @@ -456,7 +526,7 @@ export class CoreLoginSitePage { // Check if site uses SSO. const response = await this.sitesProvider.checkSite(siteUrl); - await this.sitesProvider.checkRequiredMinimumVersion(response.config); + await this.sitesProvider.checkApplication(response.config); if (!this.loginHelper.isSSOLoginNeeded(response.code)) { // No SSO, go to credentials page. diff --git a/src/core/login/providers/cron-handler.ts b/src/core/login/providers/cron-handler.ts index f821b7172..c6db2d07d 100644 --- a/src/core/login/providers/cron-handler.ts +++ b/src/core/login/providers/cron-handler.ts @@ -44,7 +44,7 @@ export class CoreLoginCronHandler implements CoreCronHandler { return site.getPublicConfig().catch(() => { return {}; }).then((config) => { - this.sitesProvider.checkRequiredMinimumVersion(config).catch(() => { + this.sitesProvider.checkApplication(config).catch(() => { // Ignore errors. }); diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 700a98d11..05c517d98 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -14,14 +14,14 @@ import { Injectable } from '@angular/core'; import { Location } from '@angular/common'; -import { Platform, AlertController, NavController, NavOptions } from 'ionic-angular'; +import { NavController, NavOptions } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider, CoreStoreConfig } from '@providers/app'; +import { CoreApp, CoreStoreConfig } from '@providers/app'; import { CoreConfigProvider } from '@providers/config'; import { CoreEventsProvider } from '@providers/events'; import { CoreInitDelegate } from '@providers/init'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreLoginSiteInfo } from '@providers/sites'; import { CoreWSProvider } from '@providers/ws'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -33,6 +33,7 @@ import { CoreConfigConstants } from '../../../configconstants'; import { CoreConstants } from '@core/constants'; import { Md5 } from 'ts-md5/dist/md5'; import { CoreSite } from '@classes/site'; +import { CoreUrl } from '@singletons/url'; /** * Data related to a SSO authentication. @@ -86,12 +87,22 @@ export class CoreLoginHelperProvider { protected isOpeningReconnect = false; waitingForBrowser = false; - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, - private wsProvider: CoreWSProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, - private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, - private urlUtils: CoreUrlUtilsProvider, private configProvider: CoreConfigProvider, private platform: Platform, - private initDelegate: CoreInitDelegate, private sitePluginsProvider: CoreSitePluginsProvider, - private location: Location, private alertCtrl: AlertController, private courseProvider: CoreCourseProvider) { + constructor( + logger: CoreLoggerProvider, + private sitesProvider: CoreSitesProvider, + private domUtils: CoreDomUtilsProvider, + private wsProvider: CoreWSProvider, + private translate: TranslateService, + private textUtils: CoreTextUtilsProvider, + private eventsProvider: CoreEventsProvider, + private utils: CoreUtilsProvider, + private urlUtils: CoreUrlUtilsProvider, + private configProvider: CoreConfigProvider, + private initDelegate: CoreInitDelegate, + private sitePluginsProvider: CoreSitePluginsProvider, + private location: Location, + private courseProvider: CoreCourseProvider + ) { this.logger = logger.getInstance('CoreLoginHelper'); this.eventsProvider.on(CoreEventsProvider.MAIN_MENU_OPEN, () => { @@ -148,17 +159,17 @@ export class CoreLoginHelperProvider { return false; } - if (this.appProvider.isSSOAuthenticationOngoing()) { + if (CoreApp.instance.isSSOAuthenticationOngoing()) { // Authentication ongoing, probably duplicated request. return true; } - if (this.appProvider.isDesktop()) { + if (CoreApp.instance.isDesktop()) { // In desktop, make sure InAppBrowser is closed. this.utils.closeInAppBrowser(true); } // App opened using custom URL scheme. Probably an SSO authentication. - this.appProvider.startSSOAuthentication(); + CoreApp.instance.startSSOAuthentication(); this.logger.debug('App launched by URL with an SSO'); // Delete the sso scheme from the URL. @@ -194,7 +205,7 @@ export class CoreLoginHelperProvider { }).then(() => { if (siteData.pageName) { // State defined, go to that state instead of site initial page. - this.appProvider.getRootNavController().push(siteData.pageName, siteData.pageParams); + CoreApp.instance.getRootNavController().push(siteData.pageName, siteData.pageParams); } else { this.goToSiteInitialPage(); } @@ -206,7 +217,7 @@ export class CoreLoginHelperProvider { } }).finally(() => { modal.dismiss(); - this.appProvider.finishSSOAuthentication(); + CoreApp.instance.finishSSOAuthentication(); }); return true; @@ -231,8 +242,8 @@ export class CoreLoginHelperProvider { * Function called when an SSO InAppBrowser is closed or the app is resumed. Check if user needs to be logged out. */ checkLogout(): void { - const navCtrl = this.appProvider.getRootNavController(); - if (!this.appProvider.isSSOAuthenticationOngoing() && this.sitesProvider.isLoggedIn() && + const navCtrl = CoreApp.instance.getRootNavController(); + if (!CoreApp.instance.isSSOAuthenticationOngoing() && this.sitesProvider.isLoggedIn() && this.sitesProvider.getCurrentSite().isLoggedOut() && navCtrl.getActive().name == 'CoreLoginReconnectPage') { // User must reauthenticate but he closed the InAppBrowser without doing so, logout him. this.sitesProvider.logout(); @@ -400,7 +411,7 @@ export class CoreLoginHelperProvider { * @return Logo URL. */ getLogoUrl(config: any): string { - return config ? (config.logourl || config.compactlogourl) : null; + return !CoreConfigConstants.forceLoginLogo && config ? (config.logourl || config.compactlogourl) : null; } /** @@ -452,7 +463,7 @@ export class CoreLoginHelperProvider { * * @return Fixed site or list of fixed sites. */ - getFixedSites(): string | any[] { + getFixedSites(): string | CoreLoginSiteInfo[] { return CoreConfigConstants.siteurl; } @@ -514,9 +525,9 @@ export class CoreLoginHelperProvider { } if (setRoot) { - return this.appProvider.getRootNavController().setRoot(pageName, params, { animate: false }); + return CoreApp.instance.getRootNavController().setRoot(pageName, params, { animate: false }); } else { - return this.appProvider.getRootNavController().push(pageName, params); + return CoreApp.instance.getRootNavController().push(pageName, params); } } @@ -529,7 +540,7 @@ export class CoreLoginHelperProvider { * @return Promise resolved when done. */ goToNoSitePage(navCtrl: NavController, page: string, params?: any): Promise { - navCtrl = navCtrl || this.appProvider.getRootNavController(); + navCtrl = navCtrl || CoreApp.instance.getRootNavController(); const currentPage = navCtrl && navCtrl.getActive().component.name; @@ -691,6 +702,35 @@ export class CoreLoginHelperProvider { return false; } + /** + * Check if a site URL is "allowed". In case the app has fixed sites, only those will be allowed to connect to. + * + * @param siteUrl Site URL to check. + * @return Promise resolved with boolean: whether is one of the fixed sites. + */ + async isSiteUrlAllowed(siteUrl: string): Promise { + if (this.isFixedUrlSet()) { + // Only 1 site allowed. + return CoreUrl.sameDomainAndPath(siteUrl, this.getFixedSites()); + } else if (this.hasSeveralFixedSites()) { + const sites = this.getFixedSites(); + + return sites.some((site) => { + return CoreUrl.sameDomainAndPath(siteUrl, site.url); + }); + } else if (CoreConfigConstants.multisitesdisplay == 'sitefinder' && CoreConfigConstants.onlyallowlistedsites) { + // Call the sites finder to validate the site. + const result = await this.sitesProvider.findSites(siteUrl.replace(/^https?\:\/\/|\.\w{2,3}\/?$/g, '')); + + return result && result.some((site) => { + return CoreUrl.sameDomainAndPath(siteUrl, site.url); + }); + } else { + // No fixed sites or it uses a non-restrictive sites finder. Allow connecting. + return true; + } + } + /** * Check if SSO login should use an embedded browser. * @@ -698,7 +738,7 @@ export class CoreLoginHelperProvider { * @return True if embedded browser, false othwerise. */ isSSOEmbeddedBrowser(code: number): boolean { - if (this.appProvider.isLinux()) { + if (CoreApp.instance.isLinux()) { // In Linux desktop app, always use embedded browser. return true; } @@ -725,7 +765,7 @@ export class CoreLoginHelperProvider { * @return Promise resolved when done. */ protected loadSiteAndPage(page: string, params: any, siteId: string): Promise { - const navCtrl = this.appProvider.getRootNavController(); + const navCtrl = CoreApp.instance.getRootNavController(); if (siteId == CoreConstants.NO_SITE_ID) { // Page doesn't belong to a site, just load the page. @@ -753,7 +793,7 @@ export class CoreLoginHelperProvider { * @param params Params to pass to the page. */ loadPageInMainMenu(page: string, params: any): void { - if (!this.appProvider.isMainMenuOpen()) { + if (!CoreApp.instance.isMainMenuOpen()) { // Main menu not open. Store the page to be loaded later. this.pageToLoad = { page: page, @@ -783,7 +823,7 @@ export class CoreLoginHelperProvider { * @return Promise resolved when done. */ protected openMainMenu(navCtrl: NavController, page: string, params: any, options?: NavOptions, url?: string): Promise { - navCtrl = navCtrl || this.appProvider.getRootNavController(); + navCtrl = navCtrl || CoreApp.instance.getRootNavController(); // Due to DeepLinker, we need to remove the path from the URL before going to main menu. // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL. @@ -827,7 +867,7 @@ export class CoreLoginHelperProvider { oauthsso: params.id, }); - if (this.appProvider.isLinux()) { + if (CoreApp.instance.isLinux()) { // In Linux desktop app, always use embedded browser. this.utils.openInApp(loginUrl); } else { @@ -944,7 +984,7 @@ export class CoreLoginHelperProvider { return; // Site that triggered the event is not current site. } - const rootNavCtrl = this.appProvider.getRootNavController(), + const rootNavCtrl = CoreApp.instance.getRootNavController(), activePage = rootNavCtrl.getActive(); // If current page is already change password, stop. @@ -1011,7 +1051,7 @@ export class CoreLoginHelperProvider { // Target page belongs to a different site. Change site. if (this.sitePluginsProvider.hasSitePluginsLoaded) { // The site has site plugins so the app will be restarted. Store the data and logout. - this.appProvider.storeRedirect(siteId, page, params); + CoreApp.instance.storeRedirect(siteId, page, params); return this.sitesProvider.logout(); } else { @@ -1026,7 +1066,7 @@ export class CoreLoginHelperProvider { if (siteId) { return this.loadSiteAndPage(page, params, siteId); } else { - return this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage'); + return CoreApp.instance.getRootNavController().setRoot('CoreLoginSitesPage'); } } @@ -1083,7 +1123,7 @@ export class CoreLoginHelperProvider { if (this.isSSOLoginNeeded(result.code)) { // SSO. User needs to authenticate in a browser. Check if we need to display a message. - if (!this.appProvider.isSSOAuthenticationOngoing() && !this.isSSOConfirmShown && !this.waitingForBrowser) { + if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.isSSOConfirmShown && !this.waitingForBrowser) { this.isSSOConfirmShown = true; if (this.shouldShowSSOConfirm(result.code)) { @@ -1095,7 +1135,6 @@ export class CoreLoginHelperProvider { promise.then(() => { this.waitingForBrowser = true; - this.sitesProvider.unsetCurrentSite(); // We need to unset current site to make authentication work fine. this.openBrowserForSSOLogin(result.siteUrl, result.code, result.service, result.config && result.config.launchurl, data.pageName, data.params); @@ -1113,11 +1152,11 @@ export class CoreLoginHelperProvider { const providerToUse = identityProviders.find((provider) => { const params = this.urlUtils.extractUrlParams(provider.url); - return params.id == currentSite.getOAuthId(); + return Number(params.id) == currentSite.getOAuthId(); }); if (providerToUse) { - if (!this.appProvider.isSSOAuthenticationOngoing() && !this.isSSOConfirmShown && !this.waitingForBrowser) { + if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.isSSOConfirmShown && !this.waitingForBrowser) { // Open browser to perform the OAuth. this.isSSOConfirmShown = true; @@ -1144,7 +1183,7 @@ export class CoreLoginHelperProvider { const info = currentSite.getInfo(); if (typeof info != 'undefined' && typeof info.username != 'undefined' && !this.isOpeningReconnect) { - const rootNavCtrl = this.appProvider.getRootNavController(), + const rootNavCtrl = CoreApp.instance.getRootNavController(), activePage = rootNavCtrl.getActive(); // If current page is already reconnect, stop. @@ -1193,9 +1232,9 @@ export class CoreLoginHelperProvider { * @param message The warning message. */ protected showWorkplaceNoticeModal(message: string): void { - const link = this.appProvider.getAppStoreUrl({android: 'com.moodle.workplace', ios: 'id1470929705' }); + const link = CoreApp.instance.getAppStoreUrl({android: 'com.moodle.workplace', ios: 'id1470929705' }); - this.showDownloadAppNoticeModal(message, link); + this.domUtils.showDownloadAppNoticeModal(message, link); } /** @@ -1209,47 +1248,9 @@ export class CoreLoginHelperProvider { storesConfig.mobile = 'https://download.moodle.org/mobile/'; storesConfig.default = 'https://download.moodle.org/mobile/'; - const link = this.appProvider.getAppStoreUrl(storesConfig); + const link = CoreApp.instance.getAppStoreUrl(storesConfig); - this.showDownloadAppNoticeModal(message, link); - } - - /** - * Show a modal warning the user that he should use a different app. - * - * @param message The warning message. - * @param link Link to the app to download if any. - */ - protected showDownloadAppNoticeModal(message: string, link?: string): void { - const buttons: any[] = [ - { - text: this.translate.instant('core.ok'), - role: 'cancel' - } - ]; - - if (link) { - buttons.push({ - text: this.translate.instant('core.download'), - handler: (): void => { - this.utils.openInBrowser(link); - } - }); - } - - const alert = this.alertCtrl.create({ - message: message, - buttons: buttons - }); - - alert.present().then(() => { - const isDevice = this.platform.is('android') || this.platform.is('ios'); - if (!isDevice) { - // Treat all anchors so they don't override the app. - const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); - this.domUtils.treatAnchors(alertMessageEl); - } - }); + this.domUtils.showDownloadAppNoticeModal(message, link); } /** @@ -1329,7 +1330,7 @@ export class CoreLoginHelperProvider { return; } - const rootNavCtrl = this.appProvider.getRootNavController(), + const rootNavCtrl = CoreApp.instance.getRootNavController(), activePage = rootNavCtrl.getActive(); // If current page is already site policy, stop. diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts index 067689bba..bc2d6e39d 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Component, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; -import { IonicPage, NavController, NavParams, Platform } from 'ionic-angular'; -import { CoreAppProvider } from '@providers/app'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { CoreApp } from '@providers/app'; import { CoreSitesProvider } from '@providers/sites'; import { CoreEventsProvider } from '@providers/events'; import { CoreIonTabsComponent } from '@components/ion-tabs/ion-tabs'; @@ -46,6 +46,7 @@ export class CoreMainMenuPage implements OnDestroy { protected urlToOpen: string; protected mainMenuId: number; protected keyboardObserver: any; + protected resizeFunction; @ViewChild('mainTabs') mainTabs: CoreIonTabsComponent; @@ -58,10 +59,9 @@ export class CoreMainMenuPage implements OnDestroy { protected mainMenuProvider: CoreMainMenuProvider, protected linksDelegate: CoreContentLinksDelegate, protected linksHelper: CoreContentLinksHelperProvider, - protected appProvider: CoreAppProvider, - protected platform: Platform) { + ) { - this.mainMenuId = this.appProvider.getMainMenuId(); + this.mainMenuId = CoreApp.instance.getMainMenuId(); // Check if the menu was loaded with a redirect. const redirectPage = navParams.get('redirectPage'); @@ -116,9 +116,10 @@ export class CoreMainMenuPage implements OnDestroy { } }); - window.addEventListener('resize', this.initHandlers.bind(this)); + this.resizeFunction = this.initHandlers.bind(this); + window.addEventListener('resize', this.resizeFunction); - if (this.platform.is('ios')) { + if (CoreApp.instance.isIOS()) { // In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done. // Init handlers again once keyboard is closed since the resize event doesn't have the updated height. this.keyboardObserver = this.eventsProvider.on(CoreEventsProvider.KEYBOARD_CHANGE, (kbHeight) => { @@ -133,7 +134,7 @@ export class CoreMainMenuPage implements OnDestroy { }); } - this.appProvider.setMainMenuOpen(this.mainMenuId, true); + CoreApp.instance.setMainMenuOpen(this.mainMenuId, true); } /** @@ -242,8 +243,8 @@ export class CoreMainMenuPage implements OnDestroy { ngOnDestroy(): void { this.subscription && this.subscription.unsubscribe(); this.redirectObs && this.redirectObs.off(); - window.removeEventListener('resize', this.initHandlers.bind(this)); - this.appProvider.setMainMenuOpen(this.mainMenuId, false); + window.removeEventListener('resize', this.resizeFunction); + CoreApp.instance.setMainMenuOpen(this.mainMenuId, false); this.keyboardObserver && this.keyboardObserver.off(); } } diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts index 9f07edc13..e044b6dd3 100644 --- a/src/core/mainmenu/providers/delegate.ts +++ b/src/core/mainmenu/providers/delegate.ts @@ -162,7 +162,7 @@ export class CoreMainMenuDelegate extends CoreDelegate { handlersData.push({ name: name, data: data, - priority: handler.priority + priority: handler.priority || 0, }); } diff --git a/src/core/pushnotifications/providers/delegate.ts b/src/core/pushnotifications/providers/delegate.ts index b8b10cbd7..6f4683590 100644 --- a/src/core/pushnotifications/providers/delegate.ts +++ b/src/core/pushnotifications/providers/delegate.ts @@ -124,7 +124,10 @@ export class CorePushNotificationsDelegate { * @return Promise resolved with boolean: whether the handler feature is disabled. */ protected isFeatureDisabled(handler: CorePushNotificationsClickHandler, siteId: string): Promise { - if (handler.featureName) { + if (!siteId) { + // Notification doesn't belong to a site. Assume all handlers are enabled. + return Promise.resolve(false); + } else if (handler.featureName) { // Check if the feature is disabled. return this.sitesProvider.isFeatureDisabled(handler.featureName, siteId); } else { @@ -177,6 +180,7 @@ export class CorePushNotificationsDelegate { this.logger.log(`Registered addon '${handler.name}'`); this.clickHandlers[handler.name] = handler; + handler.priority = handler.priority || 0; return true; } diff --git a/src/core/pushnotifications/providers/pushnotifications.ts b/src/core/pushnotifications/providers/pushnotifications.ts index fea3bbc0d..e89447fc7 100644 --- a/src/core/pushnotifications/providers/pushnotifications.ts +++ b/src/core/pushnotifications/providers/pushnotifications.ts @@ -18,7 +18,8 @@ import { Badge } from '@ionic-native/badge'; import { Push, PushObject, PushOptions } from '@ionic-native/push'; import { Device } from '@ionic-native/device'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider, CoreAppSchema } from '@providers/app'; +import { CoreApp, CoreAppProvider, CoreAppSchema } from '@providers/app'; +import { CoreEvents, CoreEventsProvider } from '@providers/events'; import { CoreInitDelegate } from '@providers/init'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; @@ -33,8 +34,6 @@ import { CoreConfigConstants } from '../../../configconstants'; import { ILocalNotification } from '@ionic-native/local-notifications'; import { SQLiteDB } from '@classes/sqlitedb'; import { CoreSite } from '@classes/site'; -import { CoreFilterProvider } from '@core/filter/providers/filter'; -import { CoreFilterDelegate } from '@core/filter/providers/delegate'; /** * Data needed to register a device in a Moodle site. @@ -178,13 +177,24 @@ export class CorePushNotificationsProvider { ], }; - constructor(logger: CoreLoggerProvider, protected appProvider: CoreAppProvider, private initDelegate: CoreInitDelegate, - protected pushNotificationsDelegate: CorePushNotificationsDelegate, protected sitesProvider: CoreSitesProvider, - private badge: Badge, private localNotificationsProvider: CoreLocalNotificationsProvider, - private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, private push: Push, - private configProvider: CoreConfigProvider, private device: Device, private zone: NgZone, - private translate: TranslateService, private platform: Platform, private sitesFactory: CoreSitesFactoryProvider, - private filterProvider: CoreFilterProvider, private filterDelegate: CoreFilterDelegate) { + constructor( + logger: CoreLoggerProvider, + private initDelegate: CoreInitDelegate, + protected pushNotificationsDelegate: CorePushNotificationsDelegate, + protected sitesProvider: CoreSitesProvider, + private badge: Badge, + private localNotificationsProvider: CoreLocalNotificationsProvider, + private utils: CoreUtilsProvider, + private textUtils: CoreTextUtilsProvider, + private push: Push, + private configProvider: CoreConfigProvider, + private device: Device, + private zone: NgZone, + private translate: TranslateService, + platform: Platform, + appProvider: CoreAppProvider, + private sitesFactory: CoreSitesFactoryProvider, + ) { this.logger = logger.getInstance('CorePushNotificationsProvider'); this.appDB = appProvider.getDB(); this.dbReady = appProvider.createTablesFromSchema(this.appTablesSchema).catch(() => { @@ -209,7 +219,7 @@ export class CorePushNotificationsProvider { * @return Whether the device can be registered in Moodle. */ canRegisterOnMoodle(): boolean { - return this.pushID && this.appProvider.isMobile(); + return this.pushID && CoreApp.instance.isMobile(); } /** @@ -234,7 +244,7 @@ export class CorePushNotificationsProvider { * @return Promise resolved when done. */ protected createDefaultChannel(): Promise { - if (!this.platform.is('android')) { + if (!CoreApp.instance.isAndroid()) { return Promise.resolve(); } @@ -443,8 +453,19 @@ export class CorePushNotificationsProvider { */ onMessageReceived(notification: any): void { const data = notification ? notification.additionalData : {}; + let promise; - this.sitesProvider.getSite(data.site).then((site) => { + if (data.site) { + promise = this.sitesProvider.getSite(data.site); + } else if (data.siteurl) { + promise = this.sitesProvider.getSiteByUrl(data.siteurl); + } else { + // Notification not related to any site. + promise = Promise.resolve(); + } + + promise.then((site: CoreSite | undefined) => { + data.site = site && site.getId(); if (typeof data.customdata == 'string') { data.customdata = this.textUtils.parseJSON(data.customdata, {}); @@ -454,74 +475,40 @@ export class CorePushNotificationsProvider { // If the app is in foreground when the notification is received, it's not shown. Let's show it ourselves. if (this.localNotificationsProvider.isAvailable()) { const localNotif: ILocalNotification = { - id: data.notId || 1, - data: data, - title: '', - text: '', - channel: 'PushPluginChannel' - }, - options = { - clean: true, - singleLine: true, - contextLevel: 'system', - instanceId: 0, - filter: true - }, - isAndroid = this.platform.is('android'), - extraFeatures = this.utils.isTrueOrOne(data.extrafeatures); + id: data.notId || 1, + data: data, + title: notification.title, + text: notification.message, + channel: 'PushPluginChannel' + }; + const isAndroid = CoreApp.instance.isAndroid(); + const extraFeatures = this.utils.isTrueOrOne(data.extrafeatures); - // Get the filters to apply to text and message. Don't use FIlterHelper to prevent circular dependencies. - this.filterProvider.canGetAvailableInContext(site.getId()).then((canGet) => { - if (!canGet) { - // We cannot check which filters are available, apply them all. - return this.filterDelegate.getEnabledFilters(options.contextLevel, options.instanceId); - } - - return this.filterProvider.getAvailableInContext(options.contextLevel, options.instanceId, site.getId()); - }).catch(() => { - return []; - }).then((filters) => { - const promises = []; - - // Apply formatText to title and message. - promises.push(this.filterProvider.formatText(notification.title, options, filters, site.getId()) - .then((title) => { - localNotif.title = title; - }).catch(() => { - localNotif.title = notification.title; - })); - - promises.push(this.filterProvider.formatText(notification.message, options, filters, site.getId()) - .catch(() => { - // Error formatting, use the original message. - return notification.message; - }).then((formattedMessage) => { - if (extraFeatures && isAndroid && this.utils.isFalseOrZero(data.notif)) { - // It's a message, use messaging style. Ionic Native doesn't specify this option. - ( localNotif).text = [ - { - message: formattedMessage, - person: data.conversationtype == 2 ? data.userfromfullname : '' - } - ]; - } else { - localNotif.text = formattedMessage; + if (extraFeatures && isAndroid && this.utils.isFalseOrZero(data.notif)) { + // It's a message, use messaging style. Ionic Native doesn't specify this option. + ( localNotif).text = [ + { + message: notification.message, + person: data.conversationtype == 2 ? data.userfromfullname : '' } - })); + ]; + } - if (extraFeatures && isAndroid) { - // Use a different icon if needed. - localNotif.icon = notification.image; - // This feature isn't supported by the official plugin, we use a fork. - ( localNotif).iconType = data['image-type']; + if (extraFeatures && isAndroid) { + // Use a different icon if needed. + localNotif.icon = notification.image; + // This feature isn't supported by the official plugin, we use a fork. + ( localNotif).iconType = data['image-type']; + + localNotif.summary = data.summaryText; + + if (data.picture) { + localNotif.attachments = [data.picture]; } + } - Promise.all(promises).then(() => { - this.localNotificationsProvider.schedule(localNotif, CorePushNotificationsProvider.COMPONENT, data.site, - true); - }); - - }); + this.localNotificationsProvider.schedule(localNotif, CorePushNotificationsProvider.COMPONENT, data.site || '', + true); } // Trigger a notification received event. @@ -544,7 +531,7 @@ export class CorePushNotificationsProvider { * @return Promise resolved when device is unregistered. */ async unregisterDeviceOnMoodle(site: CoreSite): Promise { - if (!site || !this.appProvider.isMobile()) { + if (!site || !CoreApp.instance.isMobile()) { return Promise.reject(null); } @@ -632,7 +619,7 @@ export class CorePushNotificationsProvider { return previous + parseInt(counter, 10); }, 0); - if (!this.appProvider.isDesktop() && !this.appProvider.isMobile()) { + if (!CoreApp.instance.isDesktop() && !CoreApp.instance.isMobile()) { // Browser doesn't have an app badge, stop. return total; } @@ -770,6 +757,8 @@ export class CorePushNotificationsProvider { // Now register the device. await site.write('core_user_add_user_device', this.utils.clone(data)); + CoreEvents.instance.trigger(CoreEventsProvider.DEVICE_REGISTERED_IN_MOODLE, {}, site.getId()); + // Insert the device in the local DB. try { await site.getDb().insertRecord(CorePushNotificationsProvider.REGISTERED_DEVICES_TABLE, data); diff --git a/src/core/question/classes/base-behaviour-handler.ts b/src/core/question/classes/base-behaviour-handler.ts index 852d7dfc5..50e3c7229 100644 --- a/src/core/question/classes/base-behaviour-handler.ts +++ b/src/core/question/classes/base-behaviour-handler.ts @@ -34,10 +34,11 @@ export class CoreQuestionBehaviourBaseHandler implements CoreQuestionBehaviourHa * @param component Component the question belongs to. * @param attemptId Attempt ID the question belongs to. * @param question The question. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { // Return the current state. return this.questionProvider.getState(question.state); diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts index bb0ca438a..0dbd9df63 100644 --- a/src/core/question/classes/base-question-component.ts +++ b/src/core/question/classes/base-question-component.ts @@ -14,8 +14,10 @@ import { Input, Output, EventEmitter, Injector, ElementRef } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSites } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtils } from '@providers/utils/url'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; /** @@ -30,6 +32,7 @@ export class CoreQuestionBaseComponent { @Input() contextLevel?: string; // The context level. @Input() contextInstanceId?: number; // The instance ID related to the context. @Input() courseId?: number; // The course the question belongs to (if any). + @Input() review?: boolean; // Whether the user is in review mode. @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. @@ -105,9 +108,13 @@ export class CoreQuestionBaseComponent { this.question.select = selectModel; // Check which one should be displayed first: the select or the input. - const input = questionEl.querySelector('input[type="text"][name*=answer]'); - this.question.selectFirst = - questionEl.innerHTML.indexOf(input.outerHTML) > questionEl.innerHTML.indexOf(select.outerHTML); + if (this.question.settings && this.question.settings.unitsleft !== null) { + this.question.selectFirst = this.question.settings.unitsleft == '1'; + } else { + const input = questionEl.querySelector('input[type="text"][name*=answer]'); + this.question.selectFirst = + questionEl.innerHTML.indexOf(input.outerHTML) > questionEl.innerHTML.indexOf(select.outerHTML); + } return questionEl; } @@ -159,9 +166,15 @@ export class CoreQuestionBaseComponent { } // Check which one should be displayed first: the options or the input. - const input = questionEl.querySelector('input[type="text"][name*=answer]'); - this.question.optionsFirst = - questionEl.innerHTML.indexOf(input.outerHTML) > questionEl.innerHTML.indexOf(radios[0].outerHTML); + if (this.question.settings && this.question.settings.unitsleft !== null) { + this.question.optionsFirst = this.question.settings.unitsleft == '1'; + } else { + const input = questionEl.querySelector('input[type="text"][name*=answer]'); + this.question.optionsFirst = + questionEl.innerHTML.indexOf(input.outerHTML) > questionEl.innerHTML.indexOf(radios[0].outerHTML); + } + + return questionEl; } } @@ -195,43 +208,118 @@ export class CoreQuestionBaseComponent { /** * Initialize a question component of type essay. * + * @param review Whether we're in review mode. * @return Element containing the question HTML, void if the data is not valid. */ - initEssayComponent(): void | HTMLElement { + initEssayComponent(review?: boolean): void | HTMLElement { const questionEl = this.initComponent(); - if (questionEl) { - // First search the textarea. - const textarea = questionEl.querySelector('textarea[name*=_answer]'); + if (!questionEl) { + return; + } + + const answerDraftIdInput = questionEl.querySelector('input[name*="_answer:itemid"]'); + + if (this.question.settings) { + this.question.allowsAttachments = this.question.settings.attachments != '0'; + this.question.allowsAnswerFiles = this.question.settings.responseformat == 'editorfilepicker'; + this.question.isMonospaced = this.question.settings.responseformat == 'monospaced'; + this.question.isPlainText = this.question.isMonospaced || this.question.settings.responseformat == 'plain'; + this.question.hasInlineText = this.question.settings.responseformat != 'noinline'; + } else { this.question.allowsAttachments = !!questionEl.querySelector('div[id*=filemanager]'); + this.question.allowsAnswerFiles = !!answerDraftIdInput; this.question.isMonospaced = !!questionEl.querySelector('.qtype_essay_monospaced'); this.question.isPlainText = this.question.isMonospaced || !!questionEl.querySelector('.qtype_essay_plain'); - this.question.hasDraftFiles = this.questionHelper.hasDraftFileUrls(questionEl.innerHTML); + } - if (!textarea) { - // Textarea not found, we might be in review. Search the answer and the attachments. - this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); + if (review) { + // Search the answer and the attachments. + this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); + + if (this.question.settings) { + this.question.attachments = Array.from(this.questionHelper.getResponseFileAreaFiles(this.question, 'attachments')); + } else { this.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml( this.domUtils.getContentsOfElement(questionEl, '.attachments')); - } else { - // Textarea found. - const input = questionEl.querySelector('input[type="hidden"][name*=answerformat]'), - content = textarea.innerHTML; + } - this.question.textarea = { - id: textarea.id, - name: textarea.name, - text: content ? this.textUtils.decodeHTML(content) : '' + return; + } + + const textarea = questionEl.querySelector('textarea[name*=_answer]'); + + this.question.hasDraftFiles = this.question.allowsAnswerFiles && + this.questionHelper.hasDraftFileUrls(questionEl.innerHTML); + + if (!textarea && (this.question.hasInlineText || !this.question.allowsAttachments)) { + // Textarea not found, we might be in review. Search the answer and the attachments. + this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); + this.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml( + this.domUtils.getContentsOfElement(questionEl, '.attachments')); + + return questionEl; + } + + if (textarea) { + const input = questionEl.querySelector('input[type="hidden"][name*=answerformat]'); + let content = this.textUtils.decodeHTML(textarea.innerHTML || ''); + + if (this.question.hasDraftFiles && this.question.responsefileareas) { + content = this.textUtils.replaceDraftfileUrls(CoreSites.instance.getCurrentSite().getURL(), content, + this.questionHelper.getResponseFileAreaFiles(this.question, 'answer')).text; + } + + this.question.textarea = { + id: textarea.id, + name: textarea.name, + text: content, + }; + + if (input) { + this.question.formatInput = { + name: input.name, + value: input.value }; - - if (input) { - this.question.formatInput = { - name: input.name, - value: input.value - }; - } } } + + if (answerDraftIdInput) { + this.question.answerDraftIdInput = { + name: answerDraftIdInput.name, + value: Number(answerDraftIdInput.value), + }; + } + + if (this.question.allowsAttachments) { + const attachmentsInput = questionEl.querySelector('.attachments input[name*=_attachments]'); + const objectElement = questionEl.querySelector('.attachments object'); + const fileManagerUrl = objectElement && objectElement.data; + + if (attachmentsInput) { + this.question.attachmentsDraftIdInput = { + name: attachmentsInput.name, + value: Number(attachmentsInput.value), + }; + } + + if (this.question.settings) { + this.question.attachmentsMaxFiles = Number(this.question.settings.attachments); + this.question.attachmentsAcceptedTypes = this.question.settings.filetypeslist && + this.question.settings.filetypeslist.join(','); + } + + if (fileManagerUrl) { + const params = CoreUrlUtils.instance.extractUrlParams(fileManagerUrl); + const maxBytes = Number(params.maxbytes); + const areaMaxBytes = Number(params.areamaxbytes); + + this.question.attachmentsMaxBytes = maxBytes === -1 || areaMaxBytes === -1 ? + Math.max(maxBytes, areaMaxBytes) : Math.min(maxBytes, areaMaxBytes); + } + } + + return questionEl; } /** @@ -270,6 +358,8 @@ export class CoreQuestionBaseComponent { // Set the question text. this.question.text = content.innerHTML; + + return element; } /** @@ -473,8 +563,14 @@ export class CoreQuestionBaseComponent { this.question.disabled = this.question.disabled && element.disabled; - // Get the label with the question text. - const label = questionEl.querySelector('label[for="' + option.id + '"]'); + // Get the label with the question text. Try the new format first. + const labelId = element.getAttribute('aria-labelledby'); + let label = labelId ? questionEl.querySelector('#' + labelId.replace(/:/g, '\\:')) : undefined; + if (!label) { + // Not found, use the old format. + label = questionEl.querySelector('label[for="' + option.id + '"]'); + } + if (label) { option.text = label.innerHTML; diff --git a/src/core/question/classes/base-question-handler.ts b/src/core/question/classes/base-question-handler.ts index 745536c5a..afaa35fa4 100644 --- a/src/core/question/classes/base-question-handler.ts +++ b/src/core/question/classes/base-question-handler.ts @@ -79,9 +79,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { return -1; } @@ -91,9 +93,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { return -1; } @@ -103,9 +107,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return false; } @@ -115,10 +121,13 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string) + : void | Promise { // Nothing to do. } diff --git a/src/core/question/components/question/question.ts b/src/core/question/components/question/question.ts index ce43dbf13..58bea6f95 100644 --- a/src/core/question/components/question/question.ts +++ b/src/core/question/components/question/question.ts @@ -39,6 +39,7 @@ export class CoreQuestionComponent implements OnInit { @Input() contextLevel?: string; // The context level. @Input() contextInstanceId?: number; // The instance ID related to the context. @Input() courseId?: number; // Course ID the question belongs to (if any). It can be used to improve performance with filters. + @Input() review?: boolean; // Whether the user is in review mode. @Output() buttonClicked: EventEmitter; // Will emit an event when a behaviour button is clicked. @Output() onAbort: EventEmitter; // Will emit an event if the question should be aborted. @@ -88,8 +89,9 @@ export class CoreQuestionComponent implements OnInit { contextLevel: this.contextLevel, contextInstanceId: this.contextInstanceId, courseId: this.courseId, + review: this.review, buttonClicked: this.buttonClicked, - onAbort: this.onAbort + onAbort: this.onAbort, }; // Treat the question. diff --git a/src/core/question/lang/en.json b/src/core/question/lang/en.json index 513c436d2..ee7018835 100644 --- a/src/core/question/lang/en.json +++ b/src/core/question/lang/en.json @@ -5,8 +5,8 @@ "certainty": "Certainty", "complete": "Complete", "correct": "Correct", - "errorattachmentsnotsupported": "The application doesn't support attaching files to answers yet.", - "errorinlinefilesnotsupported": "The application doesn't support editing inline files yet.", + "errorattachmentsnotsupportedinsite": "Your site doesn't support attaching files to answers yet.", + "errorembeddedfilesnotsupportedinsite": "Your site doesn't support editing embedded files yet.", "errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.", "feedback": "Feedback", "howtodraganddrop": "Tap to select then tap to drop.", diff --git a/src/core/question/providers/behaviour-delegate.ts b/src/core/question/providers/behaviour-delegate.ts index db0d052de..c0c9dd717 100644 --- a/src/core/question/providers/behaviour-delegate.ts +++ b/src/core/question/providers/behaviour-delegate.ts @@ -36,10 +36,11 @@ export interface CoreQuestionBehaviourHandler extends CoreDelegateHandler { * @param component Component the question belongs to. * @param attemptId Attempt ID the question belongs to. * @param question The question. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return State (or promise resolved with state). */ - determineNewState?(component: string, attemptId: number, question: any, siteId?: string) + determineNewState?(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise; /** @@ -74,15 +75,16 @@ export class CoreQuestionBehaviourDelegate extends CoreDelegate { * @param component Component the question belongs to. * @param attemptId Attempt ID the question belongs to. * @param question The question. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with state. */ - determineNewState(behaviour: string, component: string, attemptId: number, question: any, siteId?: string) - : Promise { + determineNewState(behaviour: string, component: string, attemptId: number, question: any, componentId: string | number, + siteId?: string): Promise { behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour); return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'determineNewState', - [component, attemptId, question, siteId])); + [component, attemptId, question, componentId, siteId])); } /** diff --git a/src/core/question/providers/delegate.ts b/src/core/question/providers/delegate.ts index 3d53a059c..7a0cd7675 100644 --- a/src/core/question/providers/delegate.ts +++ b/src/core/question/providers/delegate.ts @@ -18,6 +18,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreQuestionDefaultHandler } from './default-question-handler'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Interface that all question type handlers must implement. @@ -62,9 +63,11 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse?(question: any, answers: any): number; + isCompleteResponse?(question: any, answers: any, component: string, componentId: string | number): number; /** * Check if a student has provided enough of an answer for the question to be graded automatically, @@ -72,9 +75,11 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse?(question: any, answers: any): number; + isGradableResponse?(question: any, answers: any, component: string, componentId: string | number): number; /** * Check if two responses are the same. @@ -84,7 +89,7 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * @param newAnswers Object with the new question answers. * @return Whether they're the same. */ - isSameResponse?(question: any, prevAnswers: any, newAnswers: any): boolean; + isSameResponse?(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean; /** * Prepare and add to answers the data to send to server based in the input. Return promise if async. @@ -92,10 +97,13 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers?(question: any, answers: any, offline: boolean, siteId?: string): void | Promise; + prepareAnswers?(question: any, answers: any, offline: boolean, component: string, componentId: string | number, + siteId?: string): void | Promise; /** * Validate if an offline sequencecheck is valid compared with the online one. @@ -112,9 +120,43 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles?(question: any, usageId: number): string[]; + getAdditionalDownloadableFiles?(question: any, usageId: number): (string | CoreWSExternalFile)[]; + + /** + * Clear temporary data after the data has been saved. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + * @return If async, promise resolved when done. + */ + clearTmpData?(question: any, component: string, componentId: string | number): void | Promise; + + /** + * Delete any stored data for the question. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param siteId Site ID. If not defined, current site. + * @return If async, promise resolved when done. + */ + deleteOfflineData?(question: any, component: string, componentId: string | number, siteId?: string): void | Promise; + + /** + * Prepare data to send when performing a synchronization. + * + * @param question Question. + * @param answers Answers of the question, without the prefix. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param siteId Site ID. If not defined, current site. + * @return If async, promise resolved when done. + */ + prepareSyncData?(question: any, answers: {[name: string]: any}, component: string, componentId: string | number, + siteId?: string): void | Promise; } /** @@ -196,12 +238,14 @@ export class CoreQuestionDelegate extends CoreDelegate { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { const type = this.getTypeName(question); - return this.executeFunctionOnEnabled(type, 'isCompleteResponse', [question, answers]); + return this.executeFunctionOnEnabled(type, 'isCompleteResponse', [question, answers, component, componentId]); } /** @@ -210,12 +254,14 @@ export class CoreQuestionDelegate extends CoreDelegate { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { const type = this.getTypeName(question); - return this.executeFunctionOnEnabled(type, 'isGradableResponse', [question, answers]); + return this.executeFunctionOnEnabled(type, 'isGradableResponse', [question, answers, component, componentId]); } /** @@ -226,10 +272,10 @@ export class CoreQuestionDelegate extends CoreDelegate { * @param newAnswers Object with the new question answers. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { const type = this.getTypeName(question); - return this.executeFunctionOnEnabled(type, 'isSameResponse', [question, prevAnswers, newAnswers]); + return this.executeFunctionOnEnabled(type, 'isSameResponse', [question, prevAnswers, newAnswers, component, componentId]); } /** @@ -248,13 +294,17 @@ export class CoreQuestionDelegate extends CoreDelegate { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when data has been prepared. */ - prepareAnswersForQuestion(question: any, answers: any, offline: boolean, siteId?: string): Promise { + prepareAnswersForQuestion(question: any, answers: any, offline: boolean, component: string, componentId: string | number, + siteId?: string): Promise { const type = this.getTypeName(question); - return Promise.resolve(this.executeFunctionOnEnabled(type, 'prepareAnswers', [question, answers, offline, siteId])); + return Promise.resolve(this.executeFunctionOnEnabled(type, 'prepareAnswers', + [question, answers, offline, component, componentId, siteId])); } /** @@ -275,11 +325,57 @@ export class CoreQuestionDelegate extends CoreDelegate { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles(question: any, usageId: number): string[] { + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { const type = this.getTypeName(question); return this.executeFunctionOnEnabled(type, 'getAdditionalDownloadableFiles', [question, usageId]) || []; } + + /** + * Clear temporary data after the data has been saved. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + * @return If async, promise resolved when done. + */ + clearTmpData(question: any, component: string, componentId: string | number): void | Promise { + const type = this.getTypeName(question); + + return this.executeFunctionOnEnabled(type, 'clearTmpData', [question, component, componentId]); + } + + /** + * Clear temporary data after the data has been saved. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param siteId Site ID. If not defined, current site. + * @return If async, promise resolved when done. + */ + deleteOfflineData(question: any, component: string, componentId: string | number, siteId?: string): void | Promise { + const type = this.getTypeName(question); + + return this.executeFunctionOnEnabled(type, 'deleteOfflineData', [question, component, componentId, siteId]); + } + + /** + * Prepare data to send when performing a synchronization. + * + * @param question Question. + * @param answers Answers of the question, without the prefix. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param siteId Site ID. If not defined, current site. + * @return If async, promise resolved when done. + */ + prepareSyncData?(question: any, answers: {[name: string]: any}, component: string, componentId: string | number, + siteId?: string): void | Promise { + const type = this.getTypeName(question); + + return this.executeFunctionOnEnabled(type, 'prepareSyncData', [question, answers, component, componentId, siteId]); + } } diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index edaf17390..b90274929 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -13,9 +13,12 @@ // limitations under the License. import { Injectable, EventEmitter } from '@angular/core'; +import { FileEntry } from '@ionic-native/file'; import { TranslateService } from '@ngx-translate/core'; +import { CoreFile } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreWSExternalFile } from '@providers/ws'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; @@ -59,6 +62,41 @@ export class CoreQuestionHelperProvider { }); } + /** + * Clear questions temporary data after the data has been saved. + * + * @param questions The list of questions. + * @param component The component the question is related to. + * @param componentId Component ID. + * @return Promise resolved when done. + */ + async clearTmpData(questions: any[], component: string, componentId: string | number): Promise { + questions = questions || []; + + await Promise.all(questions.map(async (question) => { + await this.questionDelegate.clearTmpData(question, component, componentId); + })); + } + + /** + * Delete files stored for a question. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async deleteStoredQuestionFiles(question: any, component: string, componentId: string | number, siteId?: string) + : Promise { + + const questionComponentId = this.questionProvider.getQuestionComponentId(question, componentId); + const folderPath = this.questionProvider.getQuestionFolder(question.type, component, questionComponentId, siteId); + + // Ignore errors, maybe the folder doesn't exist. + await this.utils.ignoreErrors(CoreFile.instance.removeDir(folderPath)); + } + /** * Extract question behaviour submit buttons from the question's HTML and add them to "behaviourButtons" property. * The buttons aren't deleted from the content because all the im-controls block will be removed afterwards. @@ -421,6 +459,41 @@ export class CoreQuestionHelperProvider { return state ? state.class : ''; } + /** + * Return the files of a certain response file area. + * + * @param question Question. + * @param areaName Name of the area, e.g. 'attachments'. + * @return List of files. + */ + getResponseFileAreaFiles(question: any, areaName: string): CoreWSExternalFile[] { + if (!question.responsefileareas) { + return []; + } + + const area = question.responsefileareas.find((area) => { + return area.area == areaName; + }); + + return area && area.files || []; + } + + /** + * Get files stored for a question. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the files. + */ + getStoredQuestionFiles(question: any, component: string, componentId: string | number, siteId?: string): Promise { + const questionComponentId = this.questionProvider.getQuestionComponentId(question, componentId); + const folderPath = this.questionProvider.getQuestionFolder(question.type, component, questionComponentId, siteId); + + return CoreFile.instance.getDirectoryContents(folderPath); + } + /** * Get the validation error message from a question HTML if it's there. * @@ -517,29 +590,39 @@ export class CoreQuestionHelperProvider { */ prefetchQuestionFiles(question: any, component?: string, componentId?: string | number, siteId?: string, usageId?: number) : Promise { - const urls = this.filepoolProvider.extractDownloadableFilesFromHtml(question.html); if (!component) { component = CoreQuestionProvider.COMPONENT; - componentId = question.id; + componentId = question.number; } - urls.push(...this.questionDelegate.getAdditionalDownloadableFiles(question, usageId)); + const files = this.questionDelegate.getAdditionalDownloadableFiles(question, usageId) || []; + + files.push(...this.filepoolProvider.extractDownloadableFilesFromHtml(question.html)); return this.sitesProvider.getSite(siteId).then((site) => { const promises = []; + const treated = {}; - urls.forEach((url) => { - if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) { + files.forEach((file) => { + const fileUrl = typeof file == 'string' ? file : file.fileurl; + const timemodified = (typeof file != 'string' && file.timemodified) || 0; + + if (treated[fileUrl]) { + return; + } + treated[fileUrl] = true; + + if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(fileUrl)) { return; } - if (url.indexOf('theme/image.php') > -1 && url.indexOf('flagged') > -1) { + if (fileUrl.indexOf('theme/image.php') > -1 && fileUrl.indexOf('flagged') > -1) { // Ignore flag images. return; } - promises.push(this.filepoolProvider.addToQueueByUrl(siteId, url, component, componentId)); + promises.push(this.filepoolProvider.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified)); }); return Promise.all(promises); @@ -552,15 +635,19 @@ export class CoreQuestionHelperProvider { * @param questions The list of questions. * @param answers The input data. * @param offline True if data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with answers to send to server. */ - prepareAnswers(questions: any[], answers: any, offline?: boolean, siteId?: string): Promise { + prepareAnswers(questions: any[], answers: any, offline: boolean, component: string, componentId: string | number, + siteId?: string): Promise { const promises = []; questions = questions || []; questions.forEach((question) => { - promises.push(this.questionDelegate.prepareAnswersForQuestion(question, answers, offline, siteId)); + promises.push(this.questionDelegate.prepareAnswersForQuestion(question, answers, offline, component, componentId, + siteId)); }); return this.utils.allPromises(promises).then(() => { diff --git a/src/core/question/providers/question.ts b/src/core/question/providers/question.ts index 30e2ef73b..0a9945fda 100644 --- a/src/core/question/providers/question.ts +++ b/src/core/question/providers/question.ts @@ -13,10 +13,13 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreFile } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreTextUtils } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { makeSingleton } from '@singletons/core.singletons'; /** * An object to represent a question state. @@ -413,6 +416,35 @@ export class CoreQuestionProvider { }); } + /** + * Given a question and a componentId, return a componentId that is unique for the question. + * + * @param question Question. + * @param componentId Component ID. + * @return Question component ID. + */ + getQuestionComponentId(question: any, componentId: string | number): string { + return componentId + '_' + question.number; + } + + /** + * Get the path to the folder where to store files for an offline question. + * + * @param type Question type. + * @param component Component the question is related to. + * @param componentId Question component ID, returned by getQuestionComponentId. + * @param siteId Site ID. If not defined, current site. + * @return Folder path. + */ + getQuestionFolder(type: string, component: string, componentId: string, siteId?: string): string { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const siteFolderPath = CoreFile.instance.getSiteFolder(siteId); + const questionFolderPath = 'offlinequestion/' + type + '/' + component + '/' + componentId; + + return CoreTextUtils.instance.concatenatePaths(siteFolderPath, questionFolderPath); + } + /** * Extract the question slot from a question name. * @@ -612,3 +644,5 @@ export class CoreQuestionProvider { }); } } + +export class CoreQuestion extends makeSingleton(CoreQuestionProvider) {} diff --git a/src/core/settings/lang/en.json b/src/core/settings/lang/en.json index 5810653ad..33c24a3e4 100644 --- a/src/core/settings/lang/en.json +++ b/src/core/settings/lang/en.json @@ -2,10 +2,12 @@ "about": "About", "appsettings": "App settings", "appversion": "App version", + "cannotsyncloggedout": "This site cannot be synchronised because you've logged out. Please try again when you're logged in the site again.", "cannotsyncoffline": "Cannot synchronise offline.", "cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", "colorscheme": "Color Scheme", "colorscheme-auto": "Auto (based on system settings)", + "colorscheme-auto-notice": "Auto mode may not work in some Android devices.", "colorscheme-dark": "Dark", "colorscheme-light": "Light", "compilationinfo": "Compilation info", diff --git a/src/core/settings/pages/app/app.ts b/src/core/settings/pages/app/app.ts index e459cd241..e0a5058dc 100644 --- a/src/core/settings/pages/app/app.ts +++ b/src/core/settings/pages/app/app.ts @@ -13,8 +13,9 @@ // limitations under the License. import { Component, ViewChild } from '@angular/core'; -import { IonicPage, NavParams, Platform } from 'ionic-angular'; +import { IonicPage, NavParams } from 'ionic-angular'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreApp } from '@providers/app'; /** * Page that displays the list of app settings pages. @@ -30,9 +31,8 @@ export class CoreAppSettingsPage { isIOS: boolean; selectedPage: string; - constructor(platorm: Platform, - navParams: NavParams) { - this.isIOS = platorm.is('ios'); + constructor(navParams: NavParams) { + this.isIOS = CoreApp.instance.isIOS(); this.selectedPage = navParams.get('page') || false; } diff --git a/src/core/settings/pages/general/general.html b/src/core/settings/pages/general/general.html index 2cd97f99f..aa271537d 100644 --- a/src/core/settings/pages/general/general.html +++ b/src/core/settings/pages/general/general.html @@ -20,7 +20,7 @@
- +

{{ 'core.settings.colorscheme' | translate }}

{{ 'core.settings.forcedsetting' | translate }}

@@ -29,6 +29,9 @@ {{ 'core.settings.colorscheme-' + scheme | translate }}
+ +

{{ 'core.settings.colorscheme-auto-notice' | translate }}

+

{{ 'core.settings.enablerichtexteditor' | translate }}

diff --git a/src/core/settings/pages/general/general.ts b/src/core/settings/pages/general/general.ts index 2ddc61e0c..b0b03a40d 100644 --- a/src/core/settings/pages/general/general.ts +++ b/src/core/settings/pages/general/general.ts @@ -15,12 +15,12 @@ import { Component, ViewChild } from '@angular/core'; import { IonicPage, Segment } from 'ionic-angular'; import { CoreConstants } from '@core/constants'; +import { CoreApp } from '@providers/app'; import { CoreConfigProvider } from '@providers/config'; import { CoreFileProvider } from '@providers/file'; import { CoreEventsProvider } from '@providers/events'; import { CoreLangProvider } from '@providers/lang'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; import { CoreConfigConstants } from '../../../../configconstants'; import { CoreSettingsHelper } from '../../providers/helper'; @@ -47,6 +47,7 @@ export class CoreSettingsGeneralPage { colorSchemes = []; selectedScheme: string; colorSchemeDisabled: boolean; + isAndroid: boolean; constructor(protected configProvider: CoreConfigProvider, fileProvider: CoreFileProvider, @@ -54,7 +55,6 @@ export class CoreSettingsGeneralPage { protected langProvider: CoreLangProvider, protected domUtils: CoreDomUtilsProvider, protected pushNotificationsProvider: CorePushNotificationsProvider, - localNotificationsProvider: CoreLocalNotificationsProvider, protected settingsHelper: CoreSettingsHelper) { // Get the supported languages. @@ -73,17 +73,11 @@ export class CoreSettingsGeneralPage { this.colorSchemes.push('light'); this.selectedScheme = this.colorSchemes[0]; } else { - let defaultColorScheme = 'light'; + this.isAndroid = CoreApp.instance.isAndroid(); - if (window.matchMedia('(prefers-color-scheme: dark)').matches || - window.matchMedia('(prefers-color-scheme: light)').matches) { - this.colorSchemes.push('auto'); - defaultColorScheme = 'auto'; - } - this.colorSchemes.push('light'); - this.colorSchemes.push('dark'); + this.colorSchemes = this.settingsHelper.getAllowedColorSchemes(); - this.configProvider.get(CoreConstants.SETTINGS_COLOR_SCHEME, defaultColorScheme).then((scheme) => { + this.configProvider.get(CoreConstants.SETTINGS_COLOR_SCHEME, 'light').then((scheme) => { this.selectedScheme = scheme; }); } diff --git a/src/core/settings/pages/site/site.html b/src/core/settings/pages/site/site.html index 6c932d246..e03a107a1 100644 --- a/src/core/settings/pages/site/site.html +++ b/src/core/settings/pages/site/site.html @@ -33,7 +33,6 @@

{{ 'core.settings.spaceusage' | translate }}

{{ spaceUsage.spaceUsage | coreBytesToSize }}

-

{{ 'core.settings.entriesincache' | translate: { $a: spaceUsage.cacheEntries } }}

diff --git a/src/core/settings/pages/site/site.ts b/src/core/settings/pages/site/site.ts index 4f35c7feb..01e88ed56 100644 --- a/src/core/settings/pages/site/site.ts +++ b/src/core/settings/pages/site/site.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, ViewChild } from '@angular/core'; -import { IonicPage, NavParams, Platform } from 'ionic-angular'; +import { IonicPage, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreSettingsDelegate, CoreSettingsHandlerData } from '../../providers/delegate'; import { CoreEventsProvider } from '@providers/events'; @@ -22,6 +22,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSharedFilesProvider } from '@core/sharedfiles/providers/sharedfiles'; import { CoreSettingsHelper, CoreSiteSpaceUsage } from '../../providers/helper'; +import { CoreApp } from '@providers/app'; /** * Page that displays the list of site settings pages. @@ -57,11 +58,10 @@ export class CoreSitePreferencesPage { protected eventsProvider: CoreEventsProvider, protected sharedFilesProvider: CoreSharedFilesProvider, protected translate: TranslateService, - platorm: Platform, navParams: NavParams, ) { - this.isIOS = platorm.is('ios'); + this.isIOS = CoreApp.instance.isIOS(); this.selectedPage = navParams.get('page') || false; diff --git a/src/core/settings/pages/space-usage/space-usage.html b/src/core/settings/pages/space-usage/space-usage.html index 29dac4705..914cb1f9b 100644 --- a/src/core/settings/pages/space-usage/space-usage.html +++ b/src/core/settings/pages/space-usage/space-usage.html @@ -20,7 +20,6 @@

{{ site.fullName }}

{{ site.spaceUsage | coreBytesToSize }}

-

{{ 'core.settings.entriesincache' | translate: { $a: site.cacheEntries } }}