commit
						5e220c4d03
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -46,3 +46,4 @@ e2e/build | ||||
| !/desktop/assets/ | ||||
| !/desktop/electron.js | ||||
| src/configconstants.ts | ||||
| .moodleapp-dev-config | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										19
									
								
								config.xml
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								config.xml
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <widget android-versionCode="39200" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.2.0" version="3.9.2" versionCode="39200" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0"> | ||||
| <widget android-versionCode="39300" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.3.0" version="3.9.3" versionCode="39300" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0"> | ||||
|     <name>Moodle</name> | ||||
|     <description>Moodle official app</description> | ||||
|     <author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author> | ||||
| @ -56,7 +56,7 @@ | ||||
|         <resource-file src="resources/android/icon/drawable-hdpi-smallicon.png" target="app/src/main/res/mipmap-hdpi/smallicon.png" /> | ||||
|         <resource-file src="resources/android/icon/drawable-xhdpi-smallicon.png" target="app/src/main/res/mipmap-xhdpi/smallicon.png" /> | ||||
|         <edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application/activity[@android:name='MainActivity']"> | ||||
|             <activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" android:debuggable="true" /> | ||||
|             <activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" /> | ||||
|         </edit-config> | ||||
|         <edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application"> | ||||
|             <application android:largeHeap="true" android:usesCleartextTraffic="true" /> | ||||
| @ -112,11 +112,6 @@ | ||||
|                 <param name="android-package" value="io.github.pwlin.cordova.plugins.fileopener2.FileOpener2" /> | ||||
|             </feature> | ||||
|         </config-file> | ||||
|         <config-file parent="/manifest/application" target="AndroidManifest.xml"> | ||||
|             <provider android:authorities="${applicationId}.opener.provider" android:exported="false" android:grantUriPermissions="true" android:name="io.github.pwlin.cordova.plugins.fileopener2.FileProvider"> | ||||
|                 <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/opener_paths" /> | ||||
|             </provider> | ||||
|         </config-file> | ||||
|         <config-file parent="/*" target="res/xml/config.xml"> | ||||
|             <feature name="FileTransfer"> | ||||
|                 <param name="android-package" value="org.apache.cordova.filetransfer.FileTransfer" /> | ||||
| @ -219,6 +214,14 @@ | ||||
|                 </intent-filter> | ||||
|             </service> | ||||
|         </config-file> | ||||
|         <config-file parent="/*" target="res/xml/config.xml"> | ||||
|             <feature name="Media"> | ||||
|                 <param name="android-package" value="org.apache.cordova.media.AudioHandler" /> | ||||
|             </feature> | ||||
|         </config-file> | ||||
|         <config-file parent="/*" target="AndroidManifest.xml"> | ||||
|             <uses-feature android:name="android.hardware.bluetooth" android:required="false" /> | ||||
|         </config-file> | ||||
|     </platform> | ||||
|     <platform name="ios"> | ||||
|         <resource-file src="GoogleService-Info.plist" /> | ||||
| @ -241,7 +244,7 @@ | ||||
|             <true /> | ||||
|         </edit-config> | ||||
|         <edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString"> | ||||
|             <string>3.9.2</string> | ||||
|             <string>3.9.3</string> | ||||
|         </edit-config> | ||||
|         <config-file parent="FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED" target="*-Info.plist"> | ||||
|             <string>YES</string> | ||||
|  | ||||
							
								
								
									
										8
									
								
								desktop/assets/mac/loginhelper.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								desktop/assets/mac/loginhelper.plist
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
|   <dict> | ||||
|     <key>com.apple.security.app-sandbox</key> | ||||
|     <true/> | ||||
|   </dict> | ||||
| </plist> | ||||
| @ -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,10 +30,8 @@ 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" | ||||
| 
 | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|   <Identity Name="3312ADB7.MoodleDesktop" | ||||
|     ProcessorArchitecture="x64" | ||||
|     Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6" | ||||
|     Version="3.9.2.0" /> | ||||
|     Version="3.9.3.0" /> | ||||
|   <Properties> | ||||
|     <DisplayName>Moodle Desktop</DisplayName> | ||||
|     <PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName> | ||||
|  | ||||
							
								
								
									
										69
									
								
								gulp/dev-config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								gulp/dev-config.js
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
							
								
								
									
										237
									
								
								gulp/git.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								gulp/git.js
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
							
								
								
									
										475
									
								
								gulp/jira.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										475
									
								
								gulp/jira.js
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
							
								
								
									
										138
									
								
								gulp/task-build-config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								gulp/task-build-config.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										176
									
								
								gulp/task-build-lang.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								gulp/task-build-lang.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										164
									
								
								gulp/task-combine-scss.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								gulp/task-combine-scss.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										79
									
								
								gulp/task-copy-component-templates.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								gulp/task-copy-component-templates.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										280
									
								
								gulp/task-push.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								gulp/task-push.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										79
									
								
								gulp/url.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								gulp/url.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										119
									
								
								gulp/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								gulp/utils.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										469
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						
									
										469
									
								
								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); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										5986
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5986
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										90
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										90
									
								
								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" | ||||
|   } | ||||
| } | ||||
| @ -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" | ||||
|  | ||||
| @ -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; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -42,7 +42,7 @@ | ||||
|     <div class="safe-area-page"> | ||||
|         <ion-grid no-padding> | ||||
|             <ion-row no-padding> | ||||
|                 <ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-4 align-self-stretch> | ||||
|                 <ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-3 align-self-stretch> | ||||
|                     <core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true" [showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress> | ||||
|                 </ion-col> | ||||
|             </ion-row> | ||||
|  | ||||
| @ -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'; | ||||
|                 }); | ||||
| 
 | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -64,12 +64,12 @@ | ||||
|                 </a> | ||||
|             </ng-container> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <!-- Create a calendar event. --> | ||||
|         <ion-fab core-fab bottom end *ngIf="canCreate"> | ||||
|             <button ion-fab (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate"> | ||||
|                 <ion-icon name="add"></ion-icon> | ||||
|             </button> | ||||
|         </ion-fab> | ||||
|     </core-loading> | ||||
| 
 | ||||
|     <!-- Create a calendar event. --> | ||||
|     <ion-fab core-fab bottom end *ngIf="canCreate && loaded"> | ||||
|         <button ion-fab (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate"> | ||||
|             <ion-icon name="add"></ion-icon> | ||||
|         </button> | ||||
|     </ion-fab> | ||||
| </ion-content> | ||||
|  | ||||
| @ -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<void> { | ||||
| 
 | ||||
|         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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<boolean> { | ||||
|         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<AddonCompetencyDataForCourseCompetenciesPageResult> { | ||||
| 
 | ||||
|         return this.getCourseCompetenciesPage(courseId, siteId, ignoreCache).then((response) => { | ||||
| 
 | ||||
|             if (!userId || userId == this.sitesProvider.getCurrentSiteUserId()) { | ||||
|                 return response; | ||||
|             } | ||||
| 
 | ||||
|             let promises: Promise<AddonCompetencyUserCompetencySummaryInCourse>[]; | ||||
| 
 | ||||
|             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<AddonCompetencyDataForCourseCompetenciesPageResult> { | ||||
| 
 | ||||
|         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<AddonCompetencyUserCompetencySummaryInCourse>[]; | ||||
| 
 | ||||
|             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; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -11,7 +11,7 @@ | ||||
|         <ion-list> | ||||
|             <ion-item text-wrap *ngFor="let device of devices"> | ||||
|                 <ion-label [class.core-bold]="device.current"> | ||||
|                     {{ device.model }} | ||||
|                     {{ device.name }} {{ device.model }} {{ device.platform }} {{ device.version }} | ||||
|                     <span *ngIf="device.current">({{ 'core.currentdevice' | translate }})</span> | ||||
|                 </ion-label> | ||||
|                 <ion-spinner *ngIf="device.updating" item-end></ion-spinner> | ||||
|  | ||||
| @ -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<AddonMessageOutputAirnotifierDevice[]> { | ||||
|     getUserDevices(ignoreCache?: boolean, siteId?: string): Promise<AddonMessageOutputAirnotifierDevice[]> { | ||||
|         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; | ||||
|  | ||||
| @ -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<AddonMessagesConversationMessageFormatted[]> { | ||||
| 
 | ||||
|         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; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<AddonMessagesGetConversationMessagesResult> { | ||||
| 
 | ||||
|         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<AddonMessagesGetMessagesMessage[]> { | ||||
|         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. | ||||
|  */ | ||||
|  | ||||
| @ -32,8 +32,10 @@ export class AddonMessagesSettingsHandler implements CoreSettingsHandler { | ||||
|      * | ||||
|      * @return Whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return this.messagesProvider.isMessagePreferencesEnabled(); | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         const messagingEnabled = await this.messagesProvider.isPluginEnabled(); | ||||
| 
 | ||||
|         return messagingEnabled && this.messagesProvider.isMessagePreferencesEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<any> { | ||||
|     syncDiscussion(conversationId: number, userId: number, siteId?: string): Promise<string[]> { | ||||
|         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<string[]> { | ||||
|         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<any> = 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] != '<' ? '<p>' + message[textFieldName] + '</p>' : 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<string[]> { | ||||
|         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<any> { | ||||
|     protected handleSyncErrors(conversationId: number, userId: number, errors: any[], warnings: string[]): Promise<any> { | ||||
|         if (errors && errors.length) { | ||||
|             if (conversationId) { | ||||
| 
 | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline"  [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
|  | ||||
| @ -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<any> { | ||||
|         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); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ | ||||
|     </ion-item> | ||||
| 
 | ||||
|     <!-- Tabs: see the submission or grade it. --> | ||||
|     <core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true"> | ||||
|     <core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true" (ionChange)="tabSelected($event)"> | ||||
|         <!-- View the submission tab. --> | ||||
|         <core-tab [title]="'addon.mod_assign.submission' | translate"> | ||||
|             <ng-template> | ||||
|  | ||||
| @ -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<boolean> { | ||||
|     protected async hasDataToSave(isSubmit?: boolean): Promise<boolean> { | ||||
|         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<any> { | ||||
|     invalidateAndRefresh(sync?: boolean): Promise<any> { | ||||
|         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<any> { | ||||
|     protected async loadData(sync?: boolean): Promise<any> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         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<any> { | ||||
|         // 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(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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.", | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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<any> { | ||||
|     protected async fetchAssignment(sync?: boolean): Promise<void> { | ||||
|         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<any> { | ||||
|     protected refreshAllData(sync?: boolean): Promise<any> { | ||||
|         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(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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(); | ||||
|         }); | ||||
|  | ||||
| @ -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<any> { | ||||
|     syncAllAssignments(siteId?: string, force?: boolean): Promise<void> { | ||||
|         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<any> { | ||||
|     protected async syncAllAssignmentsFunc(siteId?: string, force?: boolean): Promise<void> { | ||||
|         // 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<void | AddonModAssignSyncResult> { | ||||
|         return this.isSyncNeeded(assignId, siteId).then((needed) => { | ||||
|             if (needed) { | ||||
|                 return this.syncAssign(assignId, siteId); | ||||
|             } | ||||
|         }); | ||||
|     async syncAssignIfNeeded(assignId: number, siteId?: string): Promise<void | AddonModAssignSyncResult> { | ||||
|         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<AddonModAssignSyncResult> { | ||||
|     async syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         const promises: Promise<any>[] = [], | ||||
|             result: AddonModAssignSyncResult = { | ||||
|                 warnings: [], | ||||
|                 updated: false | ||||
|             }; | ||||
|         let assign: AddonModAssignAssign, | ||||
|             courseId: number, | ||||
|             syncPromise: Promise<any>; | ||||
| 
 | ||||
|         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<AddonModAssignSyncResult> { | ||||
| 
 | ||||
|         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<any[]> { | ||||
|         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<any[]> { | ||||
|         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<any> { | ||||
|         const userId = offlineData.userid, | ||||
|             pluginData = {}; | ||||
|         let discardError, | ||||
|             submission; | ||||
|     protected async syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string) | ||||
|             : Promise<void> { | ||||
| 
 | ||||
|         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<void> { | ||||
| 
 | ||||
|         // 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<any> { | ||||
| 
 | ||||
|         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) {} | ||||
|  | ||||
| @ -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<AddonModAssignAssign> { | ||||
|         return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId); | ||||
|     getAssignment(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModAssignAssign> { | ||||
|         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<AddonModAssignAssign> { | ||||
| 
 | ||||
|         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<AddonModAssignAssign> { | ||||
|         return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId); | ||||
|     getAssignmentById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModAssignAssign> { | ||||
|         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<number> { | ||||
|         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<number> { | ||||
| 
 | ||||
|             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<AddonModAssignGrade[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                     assignmentids: [assignId] | ||||
|                 }, | ||||
|                 preSets: CoreSiteWSPreSets = { | ||||
|                     cacheKey: this.getAssignmentGradesCacheKey(assignId) | ||||
|                 }; | ||||
|     getAssignmentGrades(assignId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModAssignGrade[]> { | ||||
| 
 | ||||
|             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<AddonModAssignGetSubmissionStatusResult> { | ||||
|     getSubmissionStatus(assignId: number, options: AddonModAssignSubmissionStatusOptions = {}) | ||||
|             : Promise<AddonModAssignGetSubmissionStatusResult> { | ||||
| 
 | ||||
|         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<AddonModAssignGetSubmissionStatusResult> { | ||||
|     getSubmissionStatusWithRetry(assign: any, options: AddonModAssignSubmissionStatusOptions = {}) | ||||
|             : Promise<AddonModAssignGetSubmissionStatusResult> { | ||||
|         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<AddonModAssignParticipant[]> { | ||||
| 
 | ||||
|         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<any> { | ||||
|         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. | ||||
|  */ | ||||
|  | ||||
| @ -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<AddonModAssignParticipant[]> { | ||||
| 
 | ||||
|         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<AddonModAssignSubmissionFormatted[]> { | ||||
|             options: CoreSitesCommonWSOptions = {}): Promise<AddonModAssignSubmissionFormatted[]> { | ||||
| 
 | ||||
|         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; | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
| @ -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<boolean> { | ||||
|         // 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<any[]> { | ||||
| 
 | ||||
|         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<any> { | ||||
|         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<any> { | ||||
|         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.
 | ||||
|  | ||||
| @ -9,7 +9,7 @@ | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
|  | ||||
| @ -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<AddonModBookBook> { | ||||
|         return this.getBookByField(courseId, 'coursemodule', cmId, siteId); | ||||
|     getBook(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModBookBook> { | ||||
|         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<AddonModBookBook> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     protected getBookByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) | ||||
|             : Promise<AddonModBookBook> { | ||||
| 
 | ||||
|         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 => { | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
| @ -60,8 +60,8 @@ export class AddonModChatSessionMessagesPage { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchMessages(): Promise<any> { | ||||
|         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 = <AddonModChatSessionMessageForView[]> messages; | ||||
| 
 | ||||
|  | ||||
| @ -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 = []; | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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<AddonModChatChat> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getChat(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChatChat> { | ||||
|         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<AddonModChatGetChatUsersResult> { | ||||
|         const params = { | ||||
|             chatsid: sessionId | ||||
|         }; | ||||
|         const preSets = { | ||||
|             getFromCache: false | ||||
|         }; | ||||
|     getChatUsers(sessionId: string, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChatGetChatUsersResult> { | ||||
|         // 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<AddonModChatSession[]> { | ||||
| 
 | ||||
|         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<AddonModChatSessionMessage[]> { | ||||
|     getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, | ||||
|             options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChatSessionMessage[]> { | ||||
| 
 | ||||
|         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 => { | ||||
|  | ||||
| @ -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<any> { | ||||
|         // Prefetch chat and group info.
 | ||||
|         const promises: Promise<any>[] = [ | ||||
|             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<any> { | ||||
|         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<any> { | ||||
|         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; | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline"  [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| @ -43,13 +43,17 @@ | ||||
|     <ion-card *ngIf="options && options.length"> | ||||
|         <ng-container *ngIf="choice.allowmultiple"> | ||||
|             <ion-item text-wrap *ngFor="let option of options"> | ||||
|                 <ion-label><core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> <span *ngIf="choice.limitanswers && option.countanswers >= option.maxanswers">{{ 'addon.mod_choice.full' | translate }}</span></ion-label> | ||||
|                 <ion-label> | ||||
|                     <ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container> | ||||
|                 </ion-label> | ||||
|                 <ion-checkbox item-end [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit"></ion-checkbox> | ||||
|             </ion-item> | ||||
|         </ng-container> | ||||
|         <ng-container *ngIf="!choice.allowmultiple"> | ||||
|             <ion-item text-wrap *ngFor="let option of options" radio-group [(ngModel)]="selectedOption.id"> | ||||
|                 <ion-label><core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> <span *ngIf="choice.limitanswers && option.countanswers >= option.maxanswers">{{ 'addon.mod_choice.full' | translate }}</span></ion-label> | ||||
|                 <ion-label> | ||||
|                     <ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container> | ||||
|                 </ion-label> | ||||
|                 <ion-radio color="primary" [value]="option.id" [disabled]="option.disabled || !canEdit"></ion-radio> | ||||
|             </ion-item> | ||||
|         </ng-container> | ||||
| @ -81,6 +85,7 @@ | ||||
|                         <ion-item-divider text-wrap> | ||||
|                             <h2><core-format-text [text]="result.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text></h2> | ||||
|                             <p>{{ 'addon.mod_choice.numberofuser' | translate }}: {{ result.numberofuser }} ({{ 'core.percentagenumber' | translate: {$a: result.percentageamountfixed} }})</p> | ||||
|                             <p *ngIf="choice.limitanswers && choice.showavailable">{{ 'addon.mod_choice.limita' | translate:{$a: result.maxanswer} }}</p> | ||||
|                         </ion-item-divider> | ||||
|                         <a ion-item *ngFor="let user of result.userresponses" core-user-link [courseId]="courseid" [userId]="user.userid" [title]="user.fullname" text-wrap> | ||||
|                             <ion-avatar core-user-avatar [user]="user" item-start [courseId]="courseid"></ion-avatar> | ||||
| @ -95,3 +100,14 @@ | ||||
|         <p>{{ 'addon.mod_choice.noresultsviewable' | translate }}</p> | ||||
|     </ion-card> | ||||
| </core-loading> | ||||
| 
 | ||||
| <!-- Template to render a choice option label. --> | ||||
| <ng-template #optionLabelTemplate let-option="option"> | ||||
|     <p> | ||||
|         <core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> <span *ngIf="choice.limitanswers && option.countanswers >= option.maxanswers">{{ 'addon.mod_choice.full' | translate }}</span> | ||||
|     </p> | ||||
|     <ng-container *ngIf="choice.limitanswers && choice.showavailable"> | ||||
|         <p>{{ 'addon.mod_choice.responsesa' | translate:{$a: option.countanswers} }}</p> | ||||
|         <p>{{ 'addon.mod_choice.limita' | translate:{$a: option.maxanswers} }}</p> | ||||
|     </ng-container> | ||||
| </ng-template> | ||||
|  | ||||
| @ -174,7 +174,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchOptions(hasOffline: boolean): Promise<any> { | ||||
|         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 = []; | ||||
|  | ||||
| @ -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.", | ||||
|  | ||||
| @ -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<AddonModChoiceChoice> { | ||||
|     protected getChoiceByDataKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) | ||||
|             : Promise<AddonModChoiceChoice> { | ||||
| 
 | ||||
|         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<AddonModChoiceChoice> { | ||||
|         return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache); | ||||
|     getChoice(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChoiceChoice> { | ||||
|         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<AddonModChoiceChoice> { | ||||
|         return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache); | ||||
|     getChoiceById(courseId: number, choiceId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChoiceChoice> { | ||||
|         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<AddonModChoiceOption[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getOptions(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChoiceOption[]> { | ||||
|         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<AddonModChoiceResult[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getResults(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChoiceResult[]> { | ||||
|         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 => { | ||||
| 
 | ||||
|  | ||||
| @ -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<any> { | ||||
|         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) => { | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
|         <core-context-menu-item [priority]="500" *ngIf="canAdd" [content]="'addon.mod_data.addentries' | translate" [iconAction]="'add'" (action)="gotoAddEntries($event)"></core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="400" *ngIf="firstEntry" [content]="'addon.mod_data.single' | translate" [iconAction]="'document'" (action)="gotoEntry(firstEntry)"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="300" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
|  | ||||
| @ -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<any> { | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> { | ||||
|         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<any> { | ||||
| 
 | ||||
|         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<any> { | ||||
|     async setGroup(groupId: number): Promise<void> { | ||||
|         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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<void> { | ||||
|         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'; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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.", | ||||
|  | ||||
| @ -18,8 +18,8 @@ | ||||
|             </ion-select> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <div class="addon-data-contents addon-data-entries-{{data.id}}" *ngIf="data"> | ||||
|             <core-style [css]="data.csstemplate" prefix=".addon-data-entries-{{data.id}}"></core-style> | ||||
|         <div class="addon-data-contents {{cssClass}}" *ngIf="data"> | ||||
|             <core-style [css]="data.csstemplate" prefix=".{{cssClass}}"></core-style> | ||||
| 
 | ||||
|             <form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl> | ||||
|                 <core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html> | ||||
|  | ||||
| @ -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<any> { | ||||
|         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<void> { | ||||
|         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<any> { | ||||
|     save(e: Event): Promise<void> { | ||||
|         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<any> { | ||||
|     setGroup(groupId: number): Promise<void> { | ||||
|         this.selectedGroup = groupId; | ||||
|         this.loaded = false; | ||||
| 
 | ||||
| @ -322,7 +369,7 @@ export class AddonModDataEditPage { | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected returnToEntryList(): Promise<any> { | ||||
|     protected returnToEntryList(): Promise<void> { | ||||
|         const inputData = this.editForm.value; | ||||
| 
 | ||||
|         return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, | ||||
|  | ||||
| @ -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; | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
| @ -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<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a data to be synchronized later.
 | ||||
|         const storeOffline = (): Promise<any> => { | ||||
|             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<any> => { | ||||
|             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<any> { | ||||
|     async approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a data to be synchronized later.
 | ||||
|         const storeOffline = (): Promise<any> => { | ||||
|         const storeOffline = async (): Promise<any> => { | ||||
|             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<any> { | ||||
|     async deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a data to be synchronized later.
 | ||||
|         const storeOffline = (): Promise<any> => { | ||||
|             return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId) | ||||
|                     .then(() => { | ||||
|                 return { | ||||
|                     sent: false, | ||||
|                 }; | ||||
|             }); | ||||
|         const storeOffline = async (): Promise<any> => { | ||||
|             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<boolean> { | ||||
|         // 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<any> { | ||||
|     async editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, | ||||
|             siteId?: string, forceOffline: boolean = false): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a data to be synchronized later.
 | ||||
|         const storeOffline = (): Promise<any> => { | ||||
|             return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId) | ||||
|                     .then(() => { | ||||
|                 return { | ||||
|                     updated: true, | ||||
|                     sent: false, | ||||
|                 }; | ||||
|             }); | ||||
|         }; | ||||
|         const storeOffline = async (): Promise<any> => { | ||||
|             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<AddonModDataEntry[]> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
|     fetchAllEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise<AddonModDataEntry[]> { | ||||
|         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<AddonModDataEntry[]> { | ||||
|         return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId) | ||||
|                 .then((result) => { | ||||
|     protected fetchEntriesRecursive(dataId: number, entries: any, options: AddonModDataGetEntriesOptions = {}) | ||||
|             : Promise<AddonModDataEntry[]> { | ||||
|         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<any> { | ||||
|         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<any> { | ||||
|         return this.getDatabaseByKey(courseId, 'coursemodule', cmId, siteId, forceCache); | ||||
|     getDatabase(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.getDatabaseByKey(courseId, 'id', id, siteId, forceCache); | ||||
|     getDatabaseById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getDatabaseAccessInformation(dataId: number, options: AddonModDataAccessInfoOptions = {}): Promise<any> { | ||||
|         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<AddonModDataEntries> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise<AddonModDataEntries> { | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getFields(dataId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<AddonModDataEntries> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     searchEntries(dataId: number, options: AddonModDataSearchEntriesOptions = {}): Promise<AddonModDataEntries> { | ||||
|         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.
 | ||||
| }; | ||||
|  | ||||
| @ -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<AddonModDataEntries> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     fetchEntries(data: any, fields: any[], options: AddonModDataSearchEntriesOptions = {}): Promise<AddonModDataEntries> { | ||||
|         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<void>; | ||||
|             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<void> { | ||||
|         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(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<AddonModDataEntry[]> { | ||||
|     protected getAllUniqueEntries(dataId: number, groups: any[], options: CoreSitesCommonWSOptions = {}) | ||||
|             : Promise<AddonModDataEntry[]> { | ||||
| 
 | ||||
|         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<any> { | ||||
|     protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean, options: CoreCourseCommonModWSOptions = {}) | ||||
|             : Promise<any> { | ||||
|         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<boolean> { | ||||
|         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<any> { | ||||
|         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, | ||||
|  | ||||
| @ -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<any> { | ||||
|         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 = []; | ||||
| 
 | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline"  [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
|  | ||||
| @ -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<any> { | ||||
|         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; | ||||
| 
 | ||||
|  | ||||
| @ -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) => { | ||||
|  | ||||
| @ -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<any> { | ||||
|         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<any> { | ||||
|         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<void> { | ||||
|         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) { | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|  | ||||
| @ -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<any> { | ||||
|         return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId).then((valuesArray) => { | ||||
|     protected fillValues(feedbackId: number, items: any[], options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|     getAllNonRespondents(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise<any> { | ||||
| 
 | ||||
|         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<any> { | ||||
|     getAllResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise<any> { | ||||
| 
 | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         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<any> { | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getCurrentCompletedTimeModified(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getCurrentValues(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getFeedbackAccessInformation(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|     protected getFeedbackDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) | ||||
|             : Promise<any> { | ||||
| 
 | ||||
|         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<any> { | ||||
|         return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache, ignoreCache); | ||||
|     getFeedback(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache, ignoreCache); | ||||
|     getFeedbackById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getItems(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|     getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
|     getPageItemsWithValues(feedbackId: number, page: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<number | false> { | ||||
|         return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => { | ||||
|     protected getPageJumpTo(feedbackId: number, page: number, changePage: number, options: {cmId?: number, siteId?: string}) | ||||
|             : Promise<number | false> { | ||||
| 
 | ||||
|         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<any> { | ||||
|         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<any> { | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getResumePage(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<boolean> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     isCompleted(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<boolean> { | ||||
|         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<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
|     processPage(feedbackId: number, page: number, responses: any, options: AddonModFeedbackProcessPageOptions = {}): Promise<any> { | ||||
|         options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a message to be synchronized later.
 | ||||
|         const storeOffline = (): Promise<any> => { | ||||
|             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.;
 | ||||
| }; | ||||
|  | ||||
| @ -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<any> { | ||||
|         return this.feedbackProvider.getNonRespondents(feedbackId, groupId, page).then((responses) => { | ||||
|     getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.feedbackProvider.getResponsesAnalysis(feedbackId, groupId, page).then((responses) => { | ||||
|     getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> { | ||||
|         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, | ||||
|  | ||||
| @ -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<boolean> { | ||||
|         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<any> { | ||||
|         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); | ||||
|                     })); | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="!subfolder" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
|  | ||||
| @ -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<AddonModFolderFolder> { | ||||
|         return this.getFolderByKey(courseId, 'coursemodule', cmId, siteId); | ||||
|     getFolder(courseId: number, cmId: number, options?: CoreSitesCommonWSOptions): Promise<AddonModFolderFolder> { | ||||
|         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<AddonModFolderFolder> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     protected getFolderByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) | ||||
|             : Promise<AddonModFolderFolder> { | ||||
|         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 => { | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|         <core-context-menu-item *ngIf="loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700" [content]="'addon.mod_forum.refreshdiscussions' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && (hasOffline || hasOfflineRatings) && isOnline"  [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="sortingAvailable" [priority]="300" [content]="'core.sort' | translate" (action)="showSortOrderSelector($event)" iconAction="fa-sort"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
|     <ion-item text-wrap (click)="editPost()" *ngIf="canEdit"> | ||||
|     <ion-item text-wrap (click)="editPost()" *ngIf="offlinePost || (canEdit && isOnline)"> | ||||
|         <ion-icon name="create" item-start></ion-icon> | ||||
|         <h2>{{ 'addon.mod_forum.edit' | translate }}</h2> | ||||
|     </ion-item> | ||||
|     <ion-item text-wrap (click)="deletePost()" *ngIf="canDelete"> | ||||
|     <ion-item text-wrap (click)="deletePost()" *ngIf="offlinePost || (canDelete && isOnline)"> | ||||
|         <ion-icon name="trash" item-start></ion-icon> | ||||
|         <h2 *ngIf="post.id">{{ 'addon.mod_forum.delete' | translate }}</h2> | ||||
|         <h2 *ngIf="!post.id">{{ 'core.discard' | translate }}</h2> | ||||
|         <h2 *ngIf="!offlinePost">{{ 'addon.mod_forum.delete' | translate }}</h2> | ||||
|         <h2 *ngIf="offlinePost">{{ 'core.discard' | translate }}</h2> | ||||
|     </ion-item> | ||||
|     <ion-item text-wrap (click)="dismiss()" *ngIf="wordCount"> | ||||
|         <h2>{{ 'core.numwords' | translate: {'$a': wordCount} }}</h2> | ||||
|  | ||||
| @ -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<void> { | ||||
|         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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,11 +3,11 @@ | ||||
|         <ion-item text-wrap> | ||||
|             <div class="addon-mod-forum-post-title" *ngIf="displaySubject"> | ||||
|                 <h2 text-wrap> | ||||
|                     <core-icon name="fa-map-pin" *ngIf="post.parent == 0 && post.pinned"></core-icon> | ||||
|                     <core-icon name="fa-star" class="addon-forum-star" *ngIf="post.parent == 0 && !post.pinned && post.starred"></core-icon> | ||||
|                     <core-icon name="fa-map-pin" *ngIf="discussion && !post.parentid && discussion.pinned"></core-icon> | ||||
|                     <core-icon name="fa-star" class="addon-forum-star" *ngIf="discussion && !post.parentid && !discussion.pinned && discussion.starred"></core-icon> | ||||
|                     <core-format-text [text]="post.subject" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text> | ||||
|                 </h2> | ||||
|                 <ion-note float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate"> | ||||
|                 <ion-note float-end padding-left text-end *ngIf="trackPosts && post.unread" [attr.aria-label]="'addon.mod_forum.unread' | translate"> | ||||
|                     <core-icon name="fa-circle" color="primary"></core-icon> | ||||
|                 </ion-note> | ||||
|                 <button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)"> | ||||
| @ -15,15 +15,15 @@ | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="addon-mod-forum-post-info"> | ||||
|                 <ion-avatar *ngIf="post.userfullname" core-user-avatar [user]="post" item-start [courseId]="courseId"></ion-avatar> | ||||
|                 <ion-avatar *ngIf="post.author && post.author.fullname" core-user-avatar [user]="post.author" item-start [courseId]="courseId"></ion-avatar> | ||||
|                 <div class="addon-mod-forum-post-author"> | ||||
|                     <h3 *ngIf="post.userfullname">{{post.userfullname}}</h3> | ||||
|                     <p *ngIf="post.groupname"><ion-icon name="people"></ion-icon> {{ post.groupname }}</p> | ||||
|                     <p *ngIf="post.modified">{{post.modified * 1000 | coreFormatDate: "strftimerecentfull"}}</p> | ||||
|                     <p *ngIf="!post.modified"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p> | ||||
|                     <h3 *ngIf="post.author && post.author.fullname">{{post.author.fullname}}</h3> | ||||
|                     <p *ngIf="post.author && post.author.groups"><ng-container *ngFor="let group of post.author.groups"><ion-icon name="people"></ion-icon> {{ group.name }} </ng-container></p> | ||||
|                     <p *ngIf="post.timecreated">{{post.timecreated * 1000 | coreFormatDate: "strftimerecentfull"}}</p> | ||||
|                     <p *ngIf="!post.timecreated"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p> | ||||
|                 </div> | ||||
|                 <ng-container *ngIf="!displaySubject"> | ||||
|                     <ion-note float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate"> | ||||
|                     <ion-note float-end padding-left text-end *ngIf="trackPosts && post.unread" [attr.aria-label]="'addon.mod_forum.unread' | translate"> | ||||
|                         <core-icon name="fa-circle" color="primary"></core-icon> | ||||
|                     </ion-note> | ||||
|                     <button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)"> | ||||
| @ -33,7 +33,7 @@ | ||||
|             </div> | ||||
|         </ion-item> | ||||
|     </ion-card-header> | ||||
|     <ion-card-content [attr.padding-top]="post.parent == 0 || null"> | ||||
|     <ion-card-content [attr.padding-top]="post.parentid == 0 || null"> | ||||
|         <div padding-bottom *ngIf="post.isprivatereply"> | ||||
|             <ion-note color="danger">{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note> | ||||
|         </div> | ||||
| @ -47,17 +47,17 @@ | ||||
|             <div item-start>{{ 'core.tag.tags' | translate }}:</div> | ||||
|             <core-tag-list [tags]="post.tags"></core-tag-list> | ||||
|         </ion-item> | ||||
|         <core-rating-rate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" [userId]="post.userid" (onUpdate)="ratingUpdated()"></core-rating-rate> | ||||
|         <core-rating-rate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" [userId]="post.author.id" (onUpdate)="ratingUpdated()"></core-rating-rate> | ||||
|         <core-rating-aggregate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"></core-rating-aggregate> | ||||
| 
 | ||||
|         <ion-item no-padding text-end *ngIf="post.id && post.canreply && !post.isprivatereply" class="addon-forum-reply-button"> | ||||
|         <ion-item no-padding text-end *ngIf="post.id > 0 && post.capabilities.reply && !post.isprivatereply" class="addon-forum-reply-button"> | ||||
|             <button ion-button icon-left clear small (click)="showReplyForm()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id"> | ||||
|                 <core-icon name="fa-reply"></core-icon> {{ 'addon.mod_forum.reply' | translate }} | ||||
|             </button> | ||||
|         </ion-item> | ||||
|     </div> | ||||
| 
 | ||||
|     <form ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id && !replyData.isEditing && replyData.replyingTo == post.id) || (!post.id && replyData.isEditing && replyData.replyingTo == post.parent)" #replyFormEl> | ||||
|     <form ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id > 0 && !replyData.isEditing && replyData.replyingTo == post.id) || (post.id <=0 && replyData.isEditing && replyData.replyingTo == post.parentid)" #replyFormEl> | ||||
|         <ion-item> | ||||
|             <ion-label stacked>{{ 'addon.mod_forum.subject' | translate }}</ion-label> | ||||
|             <ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject" name="subject"></ion-input> | ||||
| @ -70,13 +70,15 @@ | ||||
|             <ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label> | ||||
|             <ion-checkbox item-end [(ngModel)]="replyData.isprivatereply" name="isprivatereply"></ion-checkbox> | ||||
|         </ion-item> | ||||
|         <ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable"> | ||||
|             <core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon> | ||||
|             <core-icon *ngIf="advanced" name="fa-caret-down" item-start></core-icon> | ||||
|             {{ 'addon.mod_forum.advanced' | translate }} | ||||
|         </ion-item-divider> | ||||
|         <ng-container *ngIf="advanced"> | ||||
|             <core-attachments *ngIf="forum.id && forum.maxattachments > 0" [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments> | ||||
|         <ng-container *ngIf="forum.id && forum.maxattachments > 0"> | ||||
|             <ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable"> | ||||
|                 <core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon> | ||||
|                 <core-icon *ngIf="advanced" name="fa-caret-down" item-start></core-icon> | ||||
|                 {{ 'addon.mod_forum.advanced' | translate }} | ||||
|             </ion-item-divider> | ||||
|             <ng-container *ngIf="advanced"> | ||||
|                 <core-attachments [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments> | ||||
|             </ng-container> | ||||
|         </ng-container> | ||||
|         <ion-grid> | ||||
|             <ion-row> | ||||
|  | ||||
| @ -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.
 | ||||
|                 })); | ||||
|             } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title *ngIf="discussion"><core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId"></core-format-text></ion-title> | ||||
|         <ion-title *ngIf="startingPost"><core-format-text [text]="startingPost.subject" contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId"></core-format-text></ion-title> | ||||
|         <ion-buttons end> | ||||
|             <!-- The context menu will be added in here. --> | ||||
|         </ion-buttons> | ||||
| @ -41,14 +41,14 @@ | ||||
|             <core-icon name="fa-lock"></core-icon> {{ 'addon.mod_forum.discussionlocked' | translate }} | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <div *ngIf="discussion" margin-bottom class="highlight"> | ||||
|             <addon-mod-forum-post [post]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post> | ||||
|         <div *ngIf="startingPost" margin-bottom class="highlight"> | ||||
|             <addon-mod-forum-post [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post> | ||||
|         </div> | ||||
| 
 | ||||
|         <ion-card *ngIf="sort != 'nested'"> | ||||
|             <ng-container *ngFor="let post of posts; first as first"> | ||||
|                 <ion-item-divider *ngIf="!first"></ion-item-divider> | ||||
|                 <addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parent]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post> | ||||
|                 <addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post> | ||||
|             </ng-container> | ||||
|         </ion-card> | ||||
| 
 | ||||
| @ -60,7 +60,7 @@ | ||||
| 
 | ||||
|         <ng-template #nestedPosts let-post="post"> | ||||
|             <ion-card> | ||||
|                 <addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parent]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post> | ||||
|                 <addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post> | ||||
|             </ion-card> | ||||
|             <div padding-left *ngIf="post.children.length && post.children[0].subject"> | ||||
|                 <ng-container *ngFor="let child of post.children"> | ||||
|  | ||||
| @ -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<void> { | ||||
|         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<SortType> { | ||||
|         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<void> { | ||||
|     protected loadDiscussion(forumId: number, cmId: number, discussionId: number): Promise<void> { | ||||
|         // 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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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<any[]> { | ||||
|         // 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; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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); | ||||
|             } | ||||
|  | ||||
| @ -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<any> { | ||||
|     canAddDiscussion(forumId: number, groupId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, siteId); | ||||
|     canAddDiscussionToAll(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<any[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise<any[]> { | ||||
|         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<any> { | ||||
|         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<any> { | ||||
| 
 | ||||
|             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<any> { | ||||
|         return this.getCourseForums(courseId, siteId).then((forums) => { | ||||
|     getForum(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.getCourseForums(courseId, siteId).then((forums) => { | ||||
|     getForumById(courseId: number, forumId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|     getAccessInformation(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         sortOrder = sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC; | ||||
|     getDiscussions(forumId: number, options: AddonModForumGetDiscussionsOptions = {}): Promise<any> { | ||||
|         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<any> { | ||||
|         if (typeof numPages == 'undefined') { | ||||
|             numPages = -1; | ||||
|         } | ||||
|         startPage = startPage || 0; | ||||
|     getDiscussionsInPages(forumId: number, options: AddonModForumGetDiscussionsInPagesOptions = {}): Promise<any> { | ||||
|         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<any> => { | ||||
|             // 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.
 | ||||
| }; | ||||
|  | ||||
| @ -161,24 +161,23 @@ export class AddonModForumHelperProvider { | ||||
|      */ | ||||
|     convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise<any> { | ||||
|         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<any> { | ||||
|     getDiscussionById(forumId: number, cmId: number, discussionId: number, siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         const findDiscussion = (page: number): Promise<any> => { | ||||
|             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); | ||||
|  | ||||
| @ -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 }) : ''; | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user