commit
						5e220c4d03
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -46,3 +46,4 @@ e2e/build | |||||||
| !/desktop/assets/ | !/desktop/assets/ | ||||||
| !/desktop/electron.js | !/desktop/electron.js | ||||||
| src/configconstants.ts | src/configconstants.ts | ||||||
|  | .moodleapp-dev-config | ||||||
|  | |||||||
| @ -52,7 +52,7 @@ jobs: | |||||||
|     script: scripts/aot.sh |     script: scripts/aot.sh | ||||||
|   - stage: build |   - stage: build | ||||||
|     name: "Build Android" |     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 |     os: linux | ||||||
|     dist: trusty |     dist: trusty | ||||||
|     group: edge |     group: edge | ||||||
| @ -69,9 +69,9 @@ jobs: | |||||||
|     script: scripts/aot.sh |     script: scripts/aot.sh | ||||||
|   - stage: build |   - stage: build | ||||||
|     name: "Build iOS" |     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 |     os: osx | ||||||
|     osx_image: xcode11.3 |     osx_image: xcode12u | ||||||
|     env: |     env: | ||||||
|     - BUILD_PLATFORM='ios' |     - BUILD_PLATFORM='ios' | ||||||
|     script: scripts/aot.sh |     script: scripts/aot.sh | ||||||
| @ -88,7 +88,7 @@ jobs: | |||||||
|     name: "Build MacOS" |     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)) |     if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch = desktop) OR (env(DEPLOY) = 3 AND tag IS NOT blank)) | ||||||
|     os: osx |     os: osx | ||||||
|     osx_image: xcode11.3 |     osx_image: xcode12u | ||||||
|     env: |     env: | ||||||
|     - ELECTRON_CACHE=$HOME/.cache/electron |     - ELECTRON_CACHE=$HOME/.cache/electron | ||||||
|     - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder |     - 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 | 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'?> | <?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> |     <name>Moodle</name> | ||||||
|     <description>Moodle official app</description> |     <description>Moodle official app</description> | ||||||
|     <author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author> |     <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-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" /> |         <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']"> |         <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> | ||||||
|         <edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application"> |         <edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application"> | ||||||
|             <application android:largeHeap="true" android:usesCleartextTraffic="true" /> |             <application android:largeHeap="true" android:usesCleartextTraffic="true" /> | ||||||
| @ -112,11 +112,6 @@ | |||||||
|                 <param name="android-package" value="io.github.pwlin.cordova.plugins.fileopener2.FileOpener2" /> |                 <param name="android-package" value="io.github.pwlin.cordova.plugins.fileopener2.FileOpener2" /> | ||||||
|             </feature> |             </feature> | ||||||
|         </config-file> |         </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"> |         <config-file parent="/*" target="res/xml/config.xml"> | ||||||
|             <feature name="FileTransfer"> |             <feature name="FileTransfer"> | ||||||
|                 <param name="android-package" value="org.apache.cordova.filetransfer.FileTransfer" /> |                 <param name="android-package" value="org.apache.cordova.filetransfer.FileTransfer" /> | ||||||
| @ -219,6 +214,14 @@ | |||||||
|                 </intent-filter> |                 </intent-filter> | ||||||
|             </service> |             </service> | ||||||
|         </config-file> |         </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> | ||||||
|     <platform name="ios"> |     <platform name="ios"> | ||||||
|         <resource-file src="GoogleService-Info.plist" /> |         <resource-file src="GoogleService-Info.plist" /> | ||||||
| @ -241,7 +244,7 @@ | |||||||
|             <true /> |             <true /> | ||||||
|         </edit-config> |         </edit-config> | ||||||
|         <edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString"> |         <edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString"> | ||||||
|             <string>3.9.2</string> |             <string>3.9.3</string> | ||||||
|         </edit-config> |         </edit-config> | ||||||
|         <config-file parent="FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED" target="*-Info.plist"> |         <config-file parent="FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED" target="*-Info.plist"> | ||||||
|             <string>YES</string> |             <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 | #!/bin/bash | ||||||
|  | # | ||||||
|  | # Script to sign macOSX pkg. | ||||||
|  | # https://www.electronjs.org/docs/tutorial/mac-app-store-submission-guide | ||||||
|  | # | ||||||
| 
 | 
 | ||||||
| # Name of your app. | # Name of your app. | ||||||
| APP="Moodle Desktop" | 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. | # The name of certificates you requested. | ||||||
| APP_KEY="3rd Party Mac Developer Application: Moodle Pty Ltd (2NU57U5PAW)" | APP_KEY="3rd Party Mac Developer Application: Moodle Pty Ltd (2NU57U5PAW)" | ||||||
| INSTALLER_KEY="3rd Party Mac Developer Installer: 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. | # The path of your plist files. | ||||||
| CHILD_PLIST="desktop/assets/mac/child.plist" | CHILD_PLIST="desktop/assets/mac/child.plist" | ||||||
| PARENT_PLIST="desktop/assets/mac/parent.plist" | PARENT_PLIST="desktop/assets/mac/parent.plist" | ||||||
|  | LOGINHELPER_PLIST="desktop/assets/mac/loginhelper.plist" | ||||||
| 
 | 
 | ||||||
| FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks" | 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/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/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.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 "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/Contents/MacOS/$APP Login Helper" | ||||||
| codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/" | 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" "$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 "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP" | codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP" | ||||||
| codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH" | codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH" | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ | |||||||
|   <Identity Name="3312ADB7.MoodleDesktop" |   <Identity Name="3312ADB7.MoodleDesktop" | ||||||
|     ProcessorArchitecture="x64" |     ProcessorArchitecture="x64" | ||||||
|     Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6" |     Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6" | ||||||
|     Version="3.9.2.0" /> |     Version="3.9.3.0" /> | ||||||
|   <Properties> |   <Properties> | ||||||
|     <DisplayName>Moodle Desktop</DisplayName> |     <DisplayName>Moodle Desktop</DisplayName> | ||||||
|     <PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName> |     <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; | ||||||
							
								
								
									
										455
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						
									
										455
									
								
								gulpfile.js
									
									
									
									
									
								
							| @ -1,193 +1,27 @@ | |||||||
| var gulp = require('gulp'), | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|     fs = require('fs'), | //
 | ||||||
|     through = require('through'), | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|     rename = require('gulp-rename'), | // you may not use this file except in compliance with the License.
 | ||||||
|     path = require('path'), | // You may obtain a copy of the License at
 | ||||||
|     slash = require('gulp-slash'), | //
 | ||||||
|     clipEmptyFiles = require('gulp-clip-empty-files'), | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|     File = require('vinyl'), | //
 | ||||||
|     flatten = require('gulp-flatten'), | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|     npmPath = require('path'), | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|     concat = require('gulp-concat'), | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|     htmlmin = require('gulp-htmlmin'), | // See the License for the specific language governing permissions and
 | ||||||
|     bufferFrom = require('buffer-from'), | // limitations under the License.
 | ||||||
|     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'; |  | ||||||
| 
 | 
 | ||||||
| /** | const BuildConfigTask = require('./gulp/task-build-config'); | ||||||
|  * Copy a property from one object to another, adding a prefix to the key if needed. | const BuildLangTask = require('./gulp/task-build-lang'); | ||||||
|  * @param {Object} target Object to copy the properties to. | const CombineScssTask = require('./gulp/task-combine-scss'); | ||||||
|  * @param {Object} source Object to copy the properties from. | const CopyComponentTemplatesTask = require('./gulp/task-copy-component-templates'); | ||||||
|  * @param {String} prefix Prefix to add to the keys. | const PushTask = require('./gulp/task-push'); | ||||||
|  */ | const Utils = require('./gulp/utils'); | ||||||
| function addProperties(target, source, prefix) { | const gulp = require('gulp'); | ||||||
|     for (var property in source) { | const pathLib = require('path'); | ||||||
|         target[prefix + property] = source[property]; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /** | const paths = { | ||||||
|  * 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\\'); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         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: [ |     lang: [ | ||||||
|         './src/lang/', |         './src/lang/', | ||||||
|         './src/core/**/lang/', |         './src/core/**/lang/', | ||||||
| @ -196,242 +30,39 @@ var paths = { | |||||||
|         './src/assets/mimetypes/' |         './src/assets/mimetypes/' | ||||||
|     ], |     ], | ||||||
|     config: './src/config.json', |     config: './src/config.json', | ||||||
|     }; | }; | ||||||
|  | 
 | ||||||
|  | const args = Utils.getCommandLineArguments(); | ||||||
| 
 | 
 | ||||||
| // Build the language files into a single file per language.
 | // Build the language files into a single file per language.
 | ||||||
| gulp.task('lang', function(done) { | gulp.task('lang', (done) => { | ||||||
|     buildLang('en', paths.lang, path.join(paths.assets, 'lang'), done); |     new BuildLangTask().run('en', paths.lang, done); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| // Convert config.json into a TypeScript class.
 | // Convert config.json into a TypeScript class.
 | ||||||
| gulp.task('config', function(done) { | gulp.task('config', (done) => { | ||||||
|     // Get the last commit.
 |     new BuildConfigTask().run(paths.config, done); | ||||||
|     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.src(paths.config) | // Copy component templates to www to make compile-html work in AOT.
 | ||||||
|             .pipe(through(function(file) { | gulp.task('copy-component-templates', (done) => { | ||||||
|                 // Convert the contents of the file into a TypeScript class.
 |     new CopyComponentTemplatesTask().run(done); | ||||||
|                 // 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; |  | ||||||
| 
 | 
 | ||||||
|                 for (var key in config) { | // Combine SCSS files.
 | ||||||
|                     var value = config[key]; | gulp.task('combine-scss', (done) => { | ||||||
|                     if (typeof value == 'string') { |     new CombineScssTask().run(done); | ||||||
|                         // 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'"); |  | ||||||
| 
 | 
 | ||||||
|                         // Check if the keys have "-" in it.
 | gulp.task('push', (done) => { | ||||||
|                         var matches = value.match(/"([^"]*\-[^"]*)":/g); |     new PushTask().run(args, done); | ||||||
|                         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('default', gulp.parallel('lang', 'config')); | gulp.task('default', gulp.parallel('lang', 'config')); | ||||||
| 
 | 
 | ||||||
| gulp.task('watch', function() { | gulp.task('watch', () => { | ||||||
|     var langsPaths = paths.lang.map(function(path) { |     const langsPaths = paths.lang.map(path => path + 'en.json'); | ||||||
|         return path + 'en.json'; | 
 | ||||||
|     }); |  | ||||||
|     gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang')); |     gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang')); | ||||||
|     gulp.watch(paths.config, { interval: 500 }, gulp.parallel('config')); |     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); |  | ||||||
| }); |  | ||||||
|  | |||||||
							
								
								
									
										5988
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5988
									
								
								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", |   "name": "moodlemobile", | ||||||
|   "version": "3.9.2", |   "version": "3.9.3", | ||||||
|   "description": "The official app for Moodle.", |   "description": "The official app for Moodle.", | ||||||
|   "author": { |   "author": { | ||||||
|     "name": "Moodle Pty Ltd.", |     "name": "Moodle Pty Ltd.", | ||||||
| @ -57,19 +57,21 @@ | |||||||
|     "@angular/platform-browser-dynamic": "5.2.11", |     "@angular/platform-browser-dynamic": "5.2.11", | ||||||
|     "@ionic-native/badge": "4.20.0", |     "@ionic-native/badge": "4.20.0", | ||||||
|     "@ionic-native/camera": "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/clipboard": "4.20.0", | ||||||
|     "@ionic-native/core": "4.20.0", |     "@ionic-native/core": "4.20.0", | ||||||
|     "@ionic-native/device": "4.20.0", |     "@ionic-native/device": "4.20.0", | ||||||
|  |     "@ionic-native/diagnostic": "4.2.0", | ||||||
|     "@ionic-native/file": "4.20.0", |     "@ionic-native/file": "4.20.0", | ||||||
|     "@ionic-native/file-opener": "4.20.0", |     "@ionic-native/file-opener": "4.20.0", | ||||||
|     "@ionic-native/file-transfer": "4.20.0", |     "@ionic-native/file-transfer": "4.20.0", | ||||||
|     "@ionic-native/geolocation": "4.20.0", |     "@ionic-native/geolocation": "4.20.0", | ||||||
|     "@ionic-native/globalization": "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/in-app-browser": "4.20.0", | ||||||
|     "@ionic-native/keyboard": "4.20.0", |     "@ionic-native/keyboard": "4.20.0", | ||||||
|     "@ionic-native/local-notifications": "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/media-capture": "4.20.0", | ||||||
|     "@ionic-native/network": "4.20.0", |     "@ionic-native/network": "4.20.0", | ||||||
|     "@ionic-native/push": "4.20.0", |     "@ionic-native/push": "4.20.0", | ||||||
| @ -85,7 +87,7 @@ | |||||||
|     "ajv": "6.11.0", |     "ajv": "6.11.0", | ||||||
|     "chart.js": "2.9.3", |     "chart.js": "2.9.3", | ||||||
|     "com-darryncampbell-cordova-plugin-intent": "1.3.0", |     "com-darryncampbell-cordova-plugin-intent": "1.3.0", | ||||||
|     "cordova": "9.0.0", |     "cordova": "10.0.0", | ||||||
|     "cordova-android": "8.1.0", |     "cordova-android": "8.1.0", | ||||||
|     "cordova-android-support-gradle-release": "3.0.1", |     "cordova-android-support-gradle-release": "3.0.1", | ||||||
|     "cordova-clipboard": "1.3.0", |     "cordova-clipboard": "1.3.0", | ||||||
| @ -93,23 +95,24 @@ | |||||||
|     "cordova-plugin-advanced-http": "2.4.1", |     "cordova-plugin-advanced-http": "2.4.1", | ||||||
|     "cordova-plugin-badge": "0.8.8", |     "cordova-plugin-badge": "0.8.8", | ||||||
|     "cordova-plugin-camera": "4.1.0", |     "cordova-plugin-camera": "4.1.0", | ||||||
|     "cordova-plugin-chooser": "1.3.1", |     "cordova-plugin-chooser": "1.3.2", | ||||||
|     "cordova-plugin-customurlscheme": "5.0.0", |     "cordova-plugin-customurlscheme": "5.0.1", | ||||||
|     "cordova-plugin-device": "2.0.3", |     "cordova-plugin-device": "2.0.3", | ||||||
|     "cordova-plugin-file": "6.0.2", |     "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-file-transfer": "1.7.1", | ||||||
|     "cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", |     "cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", | ||||||
|     "cordova-plugin-globalization": "1.11.0", |     "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-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-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-media-capture": "3.0.3", | ||||||
|     "cordova-plugin-network-information": "2.0.2", |     "cordova-plugin-network-information": "2.0.2", | ||||||
|     "cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist", |     "cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist", | ||||||
|     "cordova-plugin-screen-orientation": "3.0.2", |     "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-statusbar": "2.4.3", | ||||||
|     "cordova-plugin-whitelist": "1.3.4", |     "cordova-plugin-whitelist": "1.3.4", | ||||||
|     "cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git", |     "cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git", | ||||||
| @ -119,6 +122,7 @@ | |||||||
|     "cordova-support-google-services": "1.3.2", |     "cordova-support-google-services": "1.3.2", | ||||||
|     "es6-promise-plugin": "4.2.2", |     "es6-promise-plugin": "4.2.2", | ||||||
|     "font-awesome": "4.7.0", |     "font-awesome": "4.7.0", | ||||||
|  |     "inquirer": "^7.3.2", | ||||||
|     "ionic-angular": "3.9.9", |     "ionic-angular": "3.9.9", | ||||||
|     "ionicons": "3.0.0", |     "ionicons": "3.0.0", | ||||||
|     "jszip": "3.1.5", |     "jszip": "3.1.5", | ||||||
| @ -136,31 +140,36 @@ | |||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@ionic/app-scripts": "3.2.3", |     "@ionic/app-scripts": "3.2.3", | ||||||
|     "@ionic/cli": "^6.9.3", |     "@ionic/cli": "^6.11.7", | ||||||
|     "@types/cordova": "0.0.34", |     "@types/cordova": "^0.0.34", | ||||||
|     "@types/cordova-plugin-file-transfer": "0.0.3", |     "@types/cordova-plugin-file-transfer": "^0.0.3", | ||||||
|     "@types/cordova-plugin-globalization": "0.0.3", |     "@types/cordova-plugin-globalization": "^0.0.3", | ||||||
|     "@types/cordova-plugin-network-information": "0.0.3", |     "@types/cordova-plugin-network-information": "^0.0.3", | ||||||
|     "@types/node": "8.10.59", |     "@types/node": "^8.10.59", | ||||||
|     "@types/promise.prototype.finally": "2.0.4", |     "@types/promise.prototype.finally": "^2.0.4", | ||||||
|     "acorn": "^5.7.4", |     "acorn": "^5.7.4", | ||||||
|     "electron-builder-lib": "20.23.1", |     "cordova.plugins.diagnostic": "^5.0.2", | ||||||
|     "electron-rebuild": "1.10.0", |     "electron-builder-lib": "^20.23.1", | ||||||
|  |     "electron-rebuild": "^1.10.0", | ||||||
|     "gulp": "4.0.2", |     "gulp": "4.0.2", | ||||||
|     "gulp-clip-empty-files": "0.1.2", |     "gulp-clip-empty-files": "^0.1.2", | ||||||
|     "gulp-concat": "2.6.1", |     "gulp-concat": "^2.6.1", | ||||||
|     "gulp-flatten": "0.4.0", |     "gulp-flatten": "^0.4.0", | ||||||
|     "gulp-htmlmin": "5.0.1", |     "gulp-htmlmin": "^5.0.1", | ||||||
|     "gulp-rename": "2.0.0", |     "gulp-rename": "^2.0.0", | ||||||
|     "gulp-slash": "1.1.3", |     "gulp-slash": "^1.1.3", | ||||||
|     "lodash.template": "4.5.0", |     "lodash.template": "^4.5.0", | ||||||
|     "minimist": "^1.2.5", |     "minimist": "^1.2.5", | ||||||
|     "native-run": "^1.0.0", |     "native-run": "^1.0.0", | ||||||
|     "node-loader": "0.6.0", |     "node-loader": "^0.6.0", | ||||||
|     "through": "2.3.8", |     "request": "^2.88.2", | ||||||
|     "typescript": "2.6.2", |     "through": "^2.3.8", | ||||||
|     "vinyl": "2.2.0", |     "typescript": "~2.6.2", | ||||||
|     "webpack-merge": "4.2.2" |     "vinyl": "^2.2.0", | ||||||
|  |     "webpack-merge": "^4.2.2" | ||||||
|  |   }, | ||||||
|  |   "optionalDependencies": { | ||||||
|  |     "keytar": "^6.0.1" | ||||||
|   }, |   }, | ||||||
|   "browser": { |   "browser": { | ||||||
|     "electron": false |     "electron": false | ||||||
| @ -213,7 +222,11 @@ | |||||||
|       "cordova-plugin-wkwebview-cookies": {}, |       "cordova-plugin-wkwebview-cookies": {}, | ||||||
|       "cordova-plugin-qrscanner": {}, |       "cordova-plugin-qrscanner": {}, | ||||||
|       "cordova-plugin-chooser": {}, |       "cordova-plugin-chooser": {}, | ||||||
|       "cordova-plugin-wkuserscript": {} |       "cordova-plugin-wkuserscript": {}, | ||||||
|  |       "cordova-plugin-media": { | ||||||
|  |         "KEEP_AVAUDIOSESSION_ALWAYS_ACTIVE": "NO" | ||||||
|  |       }, | ||||||
|  |       "cordova.plugins.diagnostic": {} | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "main": "desktop/electron.js", |   "main": "desktop/electron.js", | ||||||
| @ -248,14 +261,19 @@ | |||||||
|       } |       } | ||||||
|     ], |     ], | ||||||
|     "compression": "maximum", |     "compression": "maximum", | ||||||
|     "electronVersion": "4.2.5", |     "electronVersion": "8.0.2", | ||||||
|     "mac": { |     "mac": { | ||||||
|       "category": "public.app-category.education", |       "category": "public.app-category.education", | ||||||
|       "icon": "resources/desktop/icon.icns", |       "icon": "resources/desktop/icon.icns", | ||||||
|       "target": "mas", |       "target": "mas", | ||||||
|       "bundleVersion": "3.9.2", |       "bundleVersion": "3.9.3", | ||||||
|       "extendInfo": { |       "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": { |     "win": { | ||||||
| @ -274,6 +292,6 @@ | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "engines": { |   "engines": { | ||||||
|     "node": "11.x" |     "node": ">=11.x" | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -258,7 +258,10 @@ function parse_file { | |||||||
|             value=`$exec` |             value=`$exec` | ||||||
|             guess_file $key "$value" |             guess_file $key "$value" | ||||||
|         else |         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" |                 exec="jq -r .\"$key\" $1" | ||||||
|                 value=`$exec` |                 value=`$exec` | ||||||
|                 find_better_file "$key" "$value" "$found" |                 find_better_file "$key" "$value" "$found" | ||||||
|  | |||||||
| @ -210,6 +210,18 @@ function build_lang($lang, $keys) { | |||||||
|         $string = get_translation_strings($langfoldername, $value->file, $override_langfolder); |         $string = get_translation_strings($langfoldername, $value->file, $override_langfolder); | ||||||
|         // Apply translations.
 |         // Apply translations.
 | ||||||
|         if (!$string) { |         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) { |             if (TOTRANSLATE) { | ||||||
|                 echo "\n\t\tTo translate $value->string on $value->file"; |                 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); |         $string = get_translation_strings($langfoldername, $value->file); | ||||||
|         // Apply translations.
 |         // Apply translations.
 | ||||||
|         if (!$string) { |         if (!$string) { | ||||||
|  |             // Do not count non translatable in the totals.
 | ||||||
|  |             if ($value->file == 'donottranslate') { | ||||||
|  |                 $total--; | ||||||
|  |             } | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -45,7 +45,6 @@ | |||||||
|   "addon.block_myoverview.hiddencourses": "block_myoverview", |   "addon.block_myoverview.hiddencourses": "block_myoverview", | ||||||
|   "addon.block_myoverview.inprogress": "block_myoverview", |   "addon.block_myoverview.inprogress": "block_myoverview", | ||||||
|   "addon.block_myoverview.lastaccessed": "block_myoverview", |   "addon.block_myoverview.lastaccessed": "block_myoverview", | ||||||
|   "addon.block_myoverview.morecourses": "block_myoverview", |  | ||||||
|   "addon.block_myoverview.nocourses": "block_myoverview", |   "addon.block_myoverview.nocourses": "block_myoverview", | ||||||
|   "addon.block_myoverview.past": "block_myoverview", |   "addon.block_myoverview.past": "block_myoverview", | ||||||
|   "addon.block_myoverview.pluginname": "block_myoverview", |   "addon.block_myoverview.pluginname": "block_myoverview", | ||||||
| @ -408,6 +407,7 @@ | |||||||
|   "addon.mod_assign.submitassignment_help": "assign", |   "addon.mod_assign.submitassignment_help": "assign", | ||||||
|   "addon.mod_assign.submittedearly": "assign", |   "addon.mod_assign.submittedearly": "assign", | ||||||
|   "addon.mod_assign.submittedlate": "assign", |   "addon.mod_assign.submittedlate": "assign", | ||||||
|  |   "addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp", | ||||||
|   "addon.mod_assign.timemodified": "assign", |   "addon.mod_assign.timemodified": "assign", | ||||||
|   "addon.mod_assign.timeremaining": "assign", |   "addon.mod_assign.timeremaining": "assign", | ||||||
|   "addon.mod_assign.ungroupedusers": "assign", |   "addon.mod_assign.ungroupedusers": "assign", | ||||||
| @ -463,6 +463,7 @@ | |||||||
|   "addon.mod_choice.errorgetchoice": "local_moodlemobileapp", |   "addon.mod_choice.errorgetchoice": "local_moodlemobileapp", | ||||||
|   "addon.mod_choice.expired": "choice", |   "addon.mod_choice.expired": "choice", | ||||||
|   "addon.mod_choice.full": "choice", |   "addon.mod_choice.full": "choice", | ||||||
|  |   "addon.mod_choice.limita": "choice", | ||||||
|   "addon.mod_choice.modulenameplural": "choice", |   "addon.mod_choice.modulenameplural": "choice", | ||||||
|   "addon.mod_choice.noresultsviewable": "choice", |   "addon.mod_choice.noresultsviewable": "choice", | ||||||
|   "addon.mod_choice.notopenyet": "choice", |   "addon.mod_choice.notopenyet": "choice", | ||||||
| @ -476,6 +477,7 @@ | |||||||
|   "addon.mod_choice.publishinfonever": "choice", |   "addon.mod_choice.publishinfonever": "choice", | ||||||
|   "addon.mod_choice.removemychoice": "choice", |   "addon.mod_choice.removemychoice": "choice", | ||||||
|   "addon.mod_choice.responses": "choice", |   "addon.mod_choice.responses": "choice", | ||||||
|  |   "addon.mod_choice.responsesa": "choice", | ||||||
|   "addon.mod_choice.responsesresultgraphdescription": "local_moodlemobileapp", |   "addon.mod_choice.responsesresultgraphdescription": "local_moodlemobileapp", | ||||||
|   "addon.mod_choice.responsesresultgraphheader": "choice", |   "addon.mod_choice.responsesresultgraphheader": "choice", | ||||||
|   "addon.mod_choice.resultsnotsynced": "local_moodlemobileapp", |   "addon.mod_choice.resultsnotsynced": "local_moodlemobileapp", | ||||||
| @ -505,11 +507,13 @@ | |||||||
|   "addon.mod_data.foundrecords": "data", |   "addon.mod_data.foundrecords": "data", | ||||||
|   "addon.mod_data.gettinglocation": "local_moodlemobileapp", |   "addon.mod_data.gettinglocation": "local_moodlemobileapp", | ||||||
|   "addon.mod_data.latlongboth": "data", |   "addon.mod_data.latlongboth": "data", | ||||||
|  |   "addon.mod_data.locationnotenabled": "local_moodlemobileapp", | ||||||
|   "addon.mod_data.locationpermissiondenied": "local_moodlemobileapp", |   "addon.mod_data.locationpermissiondenied": "local_moodlemobileapp", | ||||||
|   "addon.mod_data.menuchoose": "data", |   "addon.mod_data.menuchoose": "data", | ||||||
|   "addon.mod_data.modulenameplural": "data", |   "addon.mod_data.modulenameplural": "data", | ||||||
|   "addon.mod_data.more": "data", |   "addon.mod_data.more": "data", | ||||||
|   "addon.mod_data.mylocation": "local_moodlemobileapp", |   "addon.mod_data.mylocation": "local_moodlemobileapp", | ||||||
|  |   "addon.mod_data.noaccess": "data", | ||||||
|   "addon.mod_data.nomatch": "data", |   "addon.mod_data.nomatch": "data", | ||||||
|   "addon.mod_data.norecords": "data", |   "addon.mod_data.norecords": "data", | ||||||
|   "addon.mod_data.notapproved": "data", |   "addon.mod_data.notapproved": "data", | ||||||
| @ -1370,6 +1374,8 @@ | |||||||
|   "core.cannotconnecttrouble": "local_moodlemobileapp", |   "core.cannotconnecttrouble": "local_moodlemobileapp", | ||||||
|   "core.cannotconnectverify": "local_moodlemobileapp", |   "core.cannotconnectverify": "local_moodlemobileapp", | ||||||
|   "core.cannotdownloadfiles": "local_moodlemobileapp", |   "core.cannotdownloadfiles": "local_moodlemobileapp", | ||||||
|  |   "core.cannotopeninapp": "local_moodlemobileapp", | ||||||
|  |   "core.cannotopeninappdownload": "local_moodlemobileapp", | ||||||
|   "core.captureaudio": "local_moodlemobileapp", |   "core.captureaudio": "local_moodlemobileapp", | ||||||
|   "core.capturedimage": "local_moodlemobileapp", |   "core.capturedimage": "local_moodlemobileapp", | ||||||
|   "core.captureimage": "local_moodlemobileapp", |   "core.captureimage": "local_moodlemobileapp", | ||||||
| @ -1378,6 +1384,7 @@ | |||||||
|   "core.choose": "moodle", |   "core.choose": "moodle", | ||||||
|   "core.choosedots": "moodle", |   "core.choosedots": "moodle", | ||||||
|   "core.clearsearch": "local_moodlemobileapp", |   "core.clearsearch": "local_moodlemobileapp", | ||||||
|  |   "core.clearstoreddata": "local_moodlemobileapp", | ||||||
|   "core.clicktohideshow": "moodle", |   "core.clicktohideshow": "moodle", | ||||||
|   "core.clicktoseefull": "local_moodlemobileapp", |   "core.clicktoseefull": "local_moodlemobileapp", | ||||||
|   "core.close": "repository", |   "core.close": "repository", | ||||||
| @ -1431,6 +1438,7 @@ | |||||||
|   "core.course.availablespace": "local_moodlemobileapp", |   "core.course.availablespace": "local_moodlemobileapp", | ||||||
|   "core.course.cannotdeletewhiledownloading": "local_moodlemobileapp", |   "core.course.cannotdeletewhiledownloading": "local_moodlemobileapp", | ||||||
|   "core.course.confirmdeletemodulefiles": "local_moodlemobileapp", |   "core.course.confirmdeletemodulefiles": "local_moodlemobileapp", | ||||||
|  |   "core.course.confirmdeletestoreddata": "local_moodlemobileapp", | ||||||
|   "core.course.confirmdownload": "local_moodlemobileapp", |   "core.course.confirmdownload": "local_moodlemobileapp", | ||||||
|   "core.course.confirmdownloadunknownsize": "local_moodlemobileapp", |   "core.course.confirmdownloadunknownsize": "local_moodlemobileapp", | ||||||
|   "core.course.confirmdownloadzerosize": "local_moodlemobileapp", |   "core.course.confirmdownloadzerosize": "local_moodlemobileapp", | ||||||
| @ -1522,6 +1530,7 @@ | |||||||
|   "core.done": "survey", |   "core.done": "survey", | ||||||
|   "core.download": "moodle", |   "core.download": "moodle", | ||||||
|   "core.downloaded": "local_moodlemobileapp", |   "core.downloaded": "local_moodlemobileapp", | ||||||
|  |   "core.downloadfile": "moodle", | ||||||
|   "core.downloading": "local_moodlemobileapp", |   "core.downloading": "local_moodlemobileapp", | ||||||
|   "core.edit": "moodle", |   "core.edit": "moodle", | ||||||
|   "core.editor.autosavesucceeded": "editor_atto", |   "core.editor.autosavesucceeded": "editor_atto", | ||||||
| @ -1557,6 +1566,7 @@ | |||||||
|   "core.errorsomedatanotdownloaded": "local_moodlemobileapp", |   "core.errorsomedatanotdownloaded": "local_moodlemobileapp", | ||||||
|   "core.errorsync": "local_moodlemobileapp", |   "core.errorsync": "local_moodlemobileapp", | ||||||
|   "core.errorsyncblocked": "local_moodlemobileapp", |   "core.errorsyncblocked": "local_moodlemobileapp", | ||||||
|  |   "core.errorurlschemeinvalidscheme": "local_moodlemobileapp", | ||||||
|   "core.errorurlschemeinvalidsite": "local_moodlemobileapp", |   "core.errorurlschemeinvalidsite": "local_moodlemobileapp", | ||||||
|   "core.explanationdigitalminor": "moodle", |   "core.explanationdigitalminor": "moodle", | ||||||
|   "core.favourites": "moodle", |   "core.favourites": "moodle", | ||||||
| @ -1750,6 +1760,7 @@ | |||||||
|   "core.login.erroraccesscontrolalloworigin": "local_moodlemobileapp", |   "core.login.erroraccesscontrolalloworigin": "local_moodlemobileapp", | ||||||
|   "core.login.errordeletesite": "local_moodlemobileapp", |   "core.login.errordeletesite": "local_moodlemobileapp", | ||||||
|   "core.login.errorexampleurl": "local_moodlemobileapp", |   "core.login.errorexampleurl": "local_moodlemobileapp", | ||||||
|  |   "core.login.errorqrnoscheme": "local_moodlemobileapp", | ||||||
|   "core.login.errorupdatesite": "local_moodlemobileapp", |   "core.login.errorupdatesite": "local_moodlemobileapp", | ||||||
|   "core.login.faqcannotconnectanswer": "local_moodlemobileapp", |   "core.login.faqcannotconnectanswer": "local_moodlemobileapp", | ||||||
|   "core.login.faqcannotconnectquestion": "local_moodlemobileapp", |   "core.login.faqcannotconnectquestion": "local_moodlemobileapp", | ||||||
| @ -1826,7 +1837,9 @@ | |||||||
|   "core.login.selectacountry": "moodle", |   "core.login.selectacountry": "moodle", | ||||||
|   "core.login.selectsite": "local_moodlemobileapp", |   "core.login.selectsite": "local_moodlemobileapp", | ||||||
|   "core.login.signupplugindisabled": "local_moodlemobileapp", |   "core.login.signupplugindisabled": "local_moodlemobileapp", | ||||||
|  |   "core.login.signuprequiredfieldnotsupported": "local_moodlemobileapp", | ||||||
|   "core.login.siteaddress": "local_moodlemobileapp", |   "core.login.siteaddress": "local_moodlemobileapp", | ||||||
|  |   "core.login.siteaddressplaceholder": "donottranslate", | ||||||
|   "core.login.sitehasredirect": "local_moodlemobileapp", |   "core.login.sitehasredirect": "local_moodlemobileapp", | ||||||
|   "core.login.siteinmaintenance": "local_moodlemobileapp", |   "core.login.siteinmaintenance": "local_moodlemobileapp", | ||||||
|   "core.login.sitepolicynotagreederror": "local_moodlemobileapp", |   "core.login.sitepolicynotagreederror": "local_moodlemobileapp", | ||||||
| @ -1848,6 +1861,7 @@ | |||||||
|   "core.mainmenu.help": "moodle", |   "core.mainmenu.help": "moodle", | ||||||
|   "core.mainmenu.logout": "moodle", |   "core.mainmenu.logout": "moodle", | ||||||
|   "core.mainmenu.website": "local_moodlemobileapp", |   "core.mainmenu.website": "local_moodlemobileapp", | ||||||
|  |   "core.maxfilesize": "moodle", | ||||||
|   "core.maxsizeandattachments": "moodle", |   "core.maxsizeandattachments": "moodle", | ||||||
|   "core.min": "moodle", |   "core.min": "moodle", | ||||||
|   "core.mins": "moodle", |   "core.mins": "moodle", | ||||||
| @ -1899,6 +1913,7 @@ | |||||||
|   "core.noresults": "moodle", |   "core.noresults": "moodle", | ||||||
|   "core.noselection": "form", |   "core.noselection": "form", | ||||||
|   "core.notapplicable": "local_moodlemobileapp", |   "core.notapplicable": "local_moodlemobileapp", | ||||||
|  |   "core.notavailable": "moodle", | ||||||
|   "core.notenrolledprofile": "moodle", |   "core.notenrolledprofile": "moodle", | ||||||
|   "core.notice": "moodle", |   "core.notice": "moodle", | ||||||
|   "core.notingroup": "moodle", |   "core.notingroup": "moodle", | ||||||
| @ -1909,6 +1924,7 @@ | |||||||
|   "core.offline": "message", |   "core.offline": "message", | ||||||
|   "core.ok": "moodle", |   "core.ok": "moodle", | ||||||
|   "core.online": "message", |   "core.online": "message", | ||||||
|  |   "core.openfile": "local_moodlemobileapp", | ||||||
|   "core.openfullimage": "local_moodlemobileapp", |   "core.openfullimage": "local_moodlemobileapp", | ||||||
|   "core.openinbrowser": "local_moodlemobileapp", |   "core.openinbrowser": "local_moodlemobileapp", | ||||||
|   "core.openmodinbrowser": "local_moodlemobileapp", |   "core.openmodinbrowser": "local_moodlemobileapp", | ||||||
| @ -1929,8 +1945,8 @@ | |||||||
|   "core.question.certainty": "qbehaviour_deferredcbm", |   "core.question.certainty": "qbehaviour_deferredcbm", | ||||||
|   "core.question.complete": "question", |   "core.question.complete": "question", | ||||||
|   "core.question.correct": "question", |   "core.question.correct": "question", | ||||||
|   "core.question.errorattachmentsnotsupported": "local_moodlemobileapp", |   "core.question.errorattachmentsnotsupportedinsite": "local_moodlemobileapp", | ||||||
|   "core.question.errorinlinefilesnotsupported": "local_moodlemobileapp", |   "core.question.errorembeddedfilesnotsupportedinsite": "local_moodlemobileapp", | ||||||
|   "core.question.errorquestionnotsupported": "local_moodlemobileapp", |   "core.question.errorquestionnotsupported": "local_moodlemobileapp", | ||||||
|   "core.question.feedback": "question", |   "core.question.feedback": "question", | ||||||
|   "core.question.howtodraganddrop": "local_moodlemobileapp", |   "core.question.howtodraganddrop": "local_moodlemobileapp", | ||||||
| @ -1981,6 +1997,7 @@ | |||||||
|   "core.settings.about": "local_moodlemobileapp", |   "core.settings.about": "local_moodlemobileapp", | ||||||
|   "core.settings.appsettings": "local_moodlemobileapp", |   "core.settings.appsettings": "local_moodlemobileapp", | ||||||
|   "core.settings.appversion": "local_moodlemobileapp", |   "core.settings.appversion": "local_moodlemobileapp", | ||||||
|  |   "core.settings.cannotsyncloggedout": "local_moodlemobileapp", | ||||||
|   "core.settings.cannotsyncoffline": "local_moodlemobileapp", |   "core.settings.cannotsyncoffline": "local_moodlemobileapp", | ||||||
|   "core.settings.cannotsyncwithoutwifi": "local_moodlemobileapp", |   "core.settings.cannotsyncwithoutwifi": "local_moodlemobileapp", | ||||||
|   "core.settings.colorscheme": "local_moodlemobileapp", |   "core.settings.colorscheme": "local_moodlemobileapp", | ||||||
|  | |||||||
| @ -17,16 +17,7 @@ export MOODLE_DOCKER_APP_PATH=$basedir | |||||||
| print_title "Preparing dependencies" | 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/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-local_moodlemobileapp $HOME/moodle/local/moodlemobileapp | ||||||
| # git clone --branch master --depth 1 git://github.com/moodlehq/moodle-docker $HOME/moodle-docker | 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 - |  | ||||||
| 
 | 
 | ||||||
| cp $HOME/moodle-docker/config.docker-template.php $HOME/moodle/config.php | cp $HOME/moodle-docker/config.docker-template.php $HOME/moodle/config.php | ||||||
| 
 | 
 | ||||||
| @ -50,7 +41,7 @@ print_title "Running e2e tests" | |||||||
| # Run tests | # Run tests | ||||||
| for tags in "$@" | for tags in "$@" | ||||||
| do | 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" |     notify_on_error_exit "Some e2e tests are failing, please review" | ||||||
| done | done | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -42,7 +42,7 @@ | |||||||
|     <div class="safe-area-page"> |     <div class="safe-area-page"> | ||||||
|         <ion-grid no-padding> |         <ion-grid no-padding> | ||||||
|             <ion-row 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> |                     <core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true" [showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress> | ||||||
|                 </ion-col> |                 </ion-col> | ||||||
|             </ion-row> |             </ion-row> | ||||||
|  | |||||||
| @ -77,6 +77,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem | |||||||
|     downloadCourseEnabled: boolean; |     downloadCourseEnabled: boolean; | ||||||
|     downloadCoursesEnabled: boolean; |     downloadCoursesEnabled: boolean; | ||||||
| 
 | 
 | ||||||
|  |     protected FILTER_PRIORITY = ['all', 'allincludinghidden', 'inprogress', 'future', 'past', 'favourite', 'hidden', 'custom']; | ||||||
|     protected prefetchIconsInitialized = false; |     protected prefetchIconsInitialized = false; | ||||||
|     protected isDestroyed; |     protected isDestroyed; | ||||||
|     protected coursesObserver; |     protected coursesObserver; | ||||||
| @ -202,9 +203,6 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem | |||||||
| 
 | 
 | ||||||
|             this.initCourseFilters(courses); |             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.courses.filter = ''; | ||||||
|             this.showFilter = false; |             this.showFilter = false; | ||||||
| 
 | 
 | ||||||
| @ -250,10 +248,15 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem | |||||||
|                 this.showSelectorFilter = Object.keys(this.showFilters).some((key) => { |                 this.showSelectorFilter = Object.keys(this.showFilters).some((key) => { | ||||||
|                     return this.showFilters[key] == 'show'; |                     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')) { |             if (!this.showSelectorFilter) { | ||||||
|                 // No selector, or the default option is disabled, show all.
 |                 // No selector, display all the courses.
 | ||||||
|                 this.selectedFilter = 'all'; |                 this.selectedFilter = 'all'; | ||||||
|             } |             } | ||||||
|             this.setCourseFilter(this.selectedFilter); |             this.setCourseFilter(this.selectedFilter); | ||||||
| @ -388,7 +391,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem | |||||||
|             if (this.showFilters[filter] == 'show') { |             if (this.showFilters[filter] == 'show') { | ||||||
|                 this.filteredCourses = this.courses[filter]; |                 this.filteredCourses = this.courses[filter]; | ||||||
|             } else { |             } else { | ||||||
|                 const activeFilter = Object.keys(this.showFilters).find((name) => { |                 const activeFilter = this.FILTER_PRIORITY.find((name) => { | ||||||
|                     return this.showFilters[name] == 'show'; |                     return this.showFilters[name] == 'show'; | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ | |||||||
|     "hiddencourses": "Removed from view", |     "hiddencourses": "Removed from view", | ||||||
|     "inprogress": "In progress", |     "inprogress": "In progress", | ||||||
|     "lastaccessed": "Last accessed", |     "lastaccessed": "Last accessed", | ||||||
|     "morecourses": "More courses", |  | ||||||
|     "nocourses": "No courses", |     "nocourses": "No courses", | ||||||
|     "past": "Past", |     "past": "Past", | ||||||
|     "pluginname": "Course overview", |     "pluginname": "Course overview", | ||||||
|  | |||||||
| @ -64,12 +64,12 @@ | |||||||
|                 </a> |                 </a> | ||||||
|             </ng-container> |             </ng-container> | ||||||
|         </ion-list> |         </ion-list> | ||||||
|  |     </core-loading> | ||||||
| 
 | 
 | ||||||
|     <!-- Create a calendar event. --> |     <!-- Create a calendar event. --> | ||||||
|         <ion-fab core-fab bottom end *ngIf="canCreate"> |     <ion-fab core-fab bottom end *ngIf="canCreate && loaded"> | ||||||
|         <button ion-fab (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate"> |         <button ion-fab (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate"> | ||||||
|             <ion-icon name="add"></ion-icon> |             <ion-icon name="add"></ion-icon> | ||||||
|         </button> |         </button> | ||||||
|     </ion-fab> |     </ion-fab> | ||||||
|     </core-loading> |  | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -1606,10 +1606,13 @@ export class AddonCalendarProvider { | |||||||
|      * @param siteId Site ID the event belongs to. If not defined, use current site. |      * @param siteId Site ID the event belongs to. If not defined, use current site. | ||||||
|      * @return Promise resolved when the notification is scheduled. |      * @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> { |             : Promise<void> { | ||||||
| 
 | 
 | ||||||
|         if (this.localNotificationsProvider.isAvailable()) { |         if (!this.localNotificationsProvider.isAvailable()) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         if (time === 0) { |         if (time === 0) { | ||||||
| @ -1617,25 +1620,21 @@ export class AddonCalendarProvider { | |||||||
|             return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); |             return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             let promise; |  | ||||||
|         if (time == -1) { |         if (time == -1) { | ||||||
|             // If time is -1, get event default time to calculate the notification time.
 |             // If time is -1, get event default time to calculate the notification time.
 | ||||||
|                 promise = this.getDefaultNotificationTime(siteId).then((time) => { |             time = await this.getDefaultNotificationTime(siteId); | ||||||
|  | 
 | ||||||
|             if (time == 0) { |             if (time == 0) { | ||||||
|                 // Default notification time is disabled, do not show.
 |                 // Default notification time is disabled, do not show.
 | ||||||
|                 return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); |                 return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|                     return event.timestart - (time * 60); |             time = event.timestart - (time * 60); | ||||||
|                 }); |  | ||||||
|             } else { |  | ||||||
|                 promise = Promise.resolve(time); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             return promise.then((time) => { |  | ||||||
|         time = time * 1000; |         time = time * 1000; | ||||||
| 
 | 
 | ||||||
|                 if (time <= new Date().getTime()) { |         if (time <= Date.now()) { | ||||||
|             // This reminder is over, don't schedule. Cancel if it was scheduled.
 |             // This reminder is over, don't schedule. Cancel if it was scheduled.
 | ||||||
|             return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); |             return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); | ||||||
|         } |         } | ||||||
| @ -1656,11 +1655,6 @@ export class AddonCalendarProvider { | |||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|         return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId); |         return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId); | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|         } else { |  | ||||||
|             return Promise.resolve(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -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. |      * 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 courseId ID of the course. | ||||||
|      * @param userId ID of the user. |      * @param userId ID of the user. | ||||||
| @ -344,6 +377,39 @@ export class AddonCompetencyProvider { | |||||||
|     getCourseCompetencies(courseId: number, userId?: number, siteId?: string, ignoreCache?: boolean) |     getCourseCompetencies(courseId: number, userId?: number, siteId?: string, ignoreCache?: boolean) | ||||||
|             : Promise<AddonCompetencyDataForCourseCompetenciesPageResult> { |             : 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) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
| 
 | 
 | ||||||
|             this.logger.debug('Get course competencies for course ' + courseId); |             this.logger.debug('Get course competencies for course ' + courseId); | ||||||
| @ -370,26 +436,6 @@ export class AddonCompetencyProvider { | |||||||
| 
 | 
 | ||||||
|                 return Promise.reject(null); |                 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.participantsNavEnabledCache[cacheKey]; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return this.competencyProvider.getCourseCompetencies(courseId, user.id).then((response) => { |             return this.competencyProvider.canViewUserCompetenciesInCourse(courseId, user.id).then((enabled) => { | ||||||
|                 const enabled = response.competencies.length > 0; |  | ||||||
|                 this.participantsNavEnabledCache[cacheKey] = enabled; |                 this.participantsNavEnabledCache[cacheKey] = enabled; | ||||||
| 
 | 
 | ||||||
|                 return enabled; |                 return enabled; | ||||||
|             }).catch((message) => { |  | ||||||
|                 this.participantsNavEnabledCache[cacheKey] = false; |  | ||||||
| 
 |  | ||||||
|                 return false; |  | ||||||
|             }); |             }); | ||||||
|         } else { |         } else { | ||||||
|             // Link on a user site profile.
 |             // Link on a user site profile.
 | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
|  | import { CoreEventsProvider } from '@providers/events'; | ||||||
| import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; | import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; | ||||||
| import { AddonMessageOutputAirnotifierProvider } from './providers/airnotifier'; | import { AddonMessageOutputAirnotifierProvider } from './providers/airnotifier'; | ||||||
| import { AddonMessageOutputAirnotifierHandler } from './providers/handler'; | import { AddonMessageOutputAirnotifierHandler } from './providers/handler'; | ||||||
| @ -28,7 +29,13 @@ import { AddonMessageOutputAirnotifierHandler } from './providers/handler'; | |||||||
|     ] |     ] | ||||||
| }) | }) | ||||||
| export class AddonMessageOutputAirnotifierModule { | export class AddonMessageOutputAirnotifierModule { | ||||||
|     constructor(messageOutputDelegate: AddonMessageOutputDelegate, airnotifierHandler: AddonMessageOutputAirnotifierHandler) { |     constructor(messageOutputDelegate: AddonMessageOutputDelegate, airnotifierHandler: AddonMessageOutputAirnotifierHandler, | ||||||
|  |             eventsProvider: CoreEventsProvider, airnotifierProvider: AddonMessageOutputAirnotifierProvider) { | ||||||
|         messageOutputDelegate.registerHandler(airnotifierHandler); |         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-list> | ||||||
|             <ion-item text-wrap *ngFor="let device of devices"> |             <ion-item text-wrap *ngFor="let device of devices"> | ||||||
|                 <ion-label [class.core-bold]="device.current"> |                 <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> |                     <span *ngIf="device.current">({{ 'core.currentdevice' | translate }})</span> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|                 <ion-spinner *ngIf="device.updating" item-end></ion-spinner> |                 <ion-spinner *ngIf="device.updating" item-end></ion-spinner> | ||||||
|  | |||||||
| @ -74,10 +74,11 @@ export class AddonMessageOutputAirnotifierProvider { | |||||||
|     /** |     /** | ||||||
|      * Get user devices. |      * Get user devices. | ||||||
|      * |      * | ||||||
|  |      * @param ignoreCache Whether to ignore cache. | ||||||
|      * @param siteId Site ID. If not defined, use current site. |      * @param siteId Site ID. If not defined, use current site. | ||||||
|      * @return Promise resolved with the devices. |      * @return Promise resolved with the devices. | ||||||
|      */ |      */ | ||||||
|     getUserDevices(siteId?: string): Promise<AddonMessageOutputAirnotifierDevice[]> { |     getUserDevices(ignoreCache?: boolean, siteId?: string): Promise<AddonMessageOutputAirnotifierDevice[]> { | ||||||
|         this.logger.debug('Get user devices'); |         this.logger.debug('Get user devices'); | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
| @ -89,6 +90,11 @@ export class AddonMessageOutputAirnotifierProvider { | |||||||
|                 updateFrequency: CoreSite.FREQUENCY_RARELY |                 updateFrequency: CoreSite.FREQUENCY_RARELY | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|  |             if (ignoreCache) { | ||||||
|  |                 preSets['getFromCache'] = false; | ||||||
|  |                 preSets['emergencyCache'] = false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             return site.read('message_airnotifier_get_user_devices', data, preSets) |             return site.read('message_airnotifier_get_user_devices', data, preSets) | ||||||
|                     .then((data: AddonMessageOutputAirnotifierGetUserDevicesResult) => { |                     .then((data: AddonMessageOutputAirnotifierGetUserDevicesResult) => { | ||||||
|                 return data.devices; |                 return data.devices; | ||||||
|  | |||||||
| @ -581,12 +581,16 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | |||||||
|      * @param offset Offset for message list. |      * @param offset Offset for message list. | ||||||
|      * @return Promise resolved with the list of messages. |      * @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[]> { |             : Promise<AddonMessagesConversationMessageFormatted[]> { | ||||||
| 
 | 
 | ||||||
|         const excludePending = offset > 0; |         const excludePending = offset > 0; | ||||||
| 
 | 
 | ||||||
|         return this.messagesProvider.getConversationMessages(this.conversationId, excludePending, offset).then((result) => { |         const result = await this.messagesProvider.getConversationMessages(this.conversationId, { | ||||||
|  |             excludePending: excludePending, | ||||||
|  |             limitFrom: offset, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|         pagesToLoad--; |         pagesToLoad--; | ||||||
| 
 | 
 | ||||||
|         // Treat members. Don't use CoreUtilsProvider.arrayToObject because we don't want to override the existing object.
 |         // Treat members. Don't use CoreUtilsProvider.arrayToObject because we don't want to override the existing object.
 | ||||||
| @ -600,16 +604,15 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | |||||||
|             offset += AddonMessagesProvider.LIMIT_MESSAGES; |             offset += AddonMessagesProvider.LIMIT_MESSAGES; | ||||||
| 
 | 
 | ||||||
|             // Get more messages.
 |             // Get more messages.
 | ||||||
|                 return this.getConversationMessages(pagesToLoad, offset).then((nextMessages) => { |             const nextMessages = await this.getConversationMessages(pagesToLoad, offset); | ||||||
|  | 
 | ||||||
|             return result.messages.concat(nextMessages); |             return result.messages.concat(nextMessages); | ||||||
|                 }); |  | ||||||
|         } else { |         } else { | ||||||
|             // No more messages to load, return them.
 |             // No more messages to load, return them.
 | ||||||
|             this.canLoadMore = result.canLoadMore; |             this.canLoadMore = result.canLoadMore; | ||||||
| 
 | 
 | ||||||
|             return result.messages; |             return result.messages; | ||||||
|         } |         } | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; | |||||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||||
| import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper'; | import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper'; | ||||||
| import { CoreEventsProvider } from '@providers/events'; | import { CoreEventsProvider } from '@providers/events'; | ||||||
| import { CoreSite } from '@classes/site'; | import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | ||||||
| import { CoreWSExternalWarning } from '@providers/ws'; | 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. |      * @param userId The other person with whom the current user is having the discussion. | ||||||
|      * @return Cache key. |      * @return Cache key. | ||||||
|      */ |      */ | ||||||
|     protected getCacheKeyForDiscussion(userId: number): string { |     getCacheKeyForDiscussion(userId: number): string { | ||||||
|         return this.ROOT_CACHE_KEY + 'discussion:' + userId; |         return this.ROOT_CACHE_KEY + 'discussion:' + userId; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -889,56 +889,57 @@ export class AddonMessagesProvider { | |||||||
|      * Get a conversation by the conversation ID. |      * Get a conversation by the conversation ID. | ||||||
|      * |      * | ||||||
|      * @param conversationId Conversation ID to fetch. |      * @param conversationId Conversation ID to fetch. | ||||||
|      * @param excludePending True to exclude messages pending to be sent. |      * @param options Options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved with the response. |      * @return Promise resolved with the response. | ||||||
|      * @since 3.6 |      * @since 3.6 | ||||||
|      */ |      */ | ||||||
|     getConversationMessages(conversationId: number, excludePending: boolean, limitFrom: number = 0, limitTo?: number, |     async getConversationMessages(conversationId: number, options?: AddonMessagesGetConversationMessagesOptions) | ||||||
|             newestFirst: boolean = true, timeFrom: number = 0, siteId?: string, userId?: number) |  | ||||||
|             : Promise<AddonMessagesGetConversationMessagesResult> { |             : Promise<AddonMessagesGetConversationMessagesResult> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         options = options || {}; | ||||||
|             userId = userId || site.getUserId(); |  | ||||||
| 
 | 
 | ||||||
|             if (typeof limitTo == 'undefined' || limitTo === null) { |         const site = await this.sitesProvider.getSite(options.siteId); | ||||||
|                 limitTo = this.LIMIT_MESSAGES; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             const preSets = { |         options.userId = options.userId || site.getUserId(); | ||||||
|                     cacheKey: this.getCacheKeyForConversationMessages(userId, conversationId) |         options.limitFrom = options.limitFrom || 0; | ||||||
|                 }, |         options.limitTo = options.limitTo === undefined || options.limitTo === null ? this.LIMIT_MESSAGES : options.limitTo; | ||||||
|                 params: any = { |         options.timeFrom = options.timeFrom || 0; | ||||||
|                     currentuserid: userId, |         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, |             convid: conversationId, | ||||||
|                     limitfrom: limitFrom, |             limitfrom: options.limitFrom, | ||||||
|                     limitnum: limitTo < 1 ? limitTo : limitTo + 1, // If there is a limit, get 1 more than requested.
 |             limitnum: options.limitTo < 1 ? options.limitTo : options.limitTo + 1, // If there's a limit, get 1 more than requested.
 | ||||||
|                     newest: newestFirst ? 1 : 0, |             newest: options.newestFirst ? 1 : 0, | ||||||
|                     timefrom: timeFrom |             timefrom: options.timeFrom, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|             if (limitFrom > 0) { |         if (options.limitFrom > 0) { | ||||||
|             // Do not use cache when retrieving older messages.
 |             // Do not use cache when retrieving older messages.
 | ||||||
|             // This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded.
 |             // This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded.
 | ||||||
|                 preSets['getFromCache'] = false; |             preSets.getFromCache = false; | ||||||
|                 preSets['saveToCache'] = false; |             preSets.saveToCache = false; | ||||||
|                 preSets['emergencyCache'] = false; |             preSets.emergencyCache = false; | ||||||
|  |         } else if (options.forceCache) { | ||||||
|  |             preSets.omitExpires = true; | ||||||
|  |         } else if (options.ignoreCache) { | ||||||
|  |             preSets.getFromCache = false; | ||||||
|  |             preSets.emergencyCache = false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             return site.read('core_message_get_conversation_messages', params, preSets) |         const result: AddonMessagesGetConversationMessagesResult = | ||||||
|                     .then((result: AddonMessagesGetConversationMessagesResult) => { |                 await site.read('core_message_get_conversation_messages', params, preSets); | ||||||
| 
 | 
 | ||||||
|                 if (limitTo < 1) { |         if (options.limitTo < 1) { | ||||||
|             result.canLoadMore = false; |             result.canLoadMore = false; | ||||||
|             result.messages = result.messages; |             result.messages = result.messages; | ||||||
|         } else { |         } else { | ||||||
|                     result.canLoadMore = result.messages.length > limitTo; |             result.canLoadMore = result.messages.length > options.limitTo; | ||||||
|                     result.messages = result.messages.slice(0, limitTo); |             result.messages = result.messages.slice(0, options.limitTo); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let lastReceived; |         let lastReceived; | ||||||
| @ -947,35 +948,33 @@ export class AddonMessagesProvider { | |||||||
|             // Convert time to milliseconds.
 |             // Convert time to milliseconds.
 | ||||||
|             message.timecreated = message.timecreated ? message.timecreated * 1000 : 0; |             message.timecreated = message.timecreated ? message.timecreated * 1000 : 0; | ||||||
| 
 | 
 | ||||||
|                     if (!lastReceived && message.useridfrom != userId) { |             if (!lastReceived && message.useridfrom != options.userId) { | ||||||
|                 lastReceived = message; |                 lastReceived = message; | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|                 if (this.appProvider.isDesktop() && limitFrom === 0 && lastReceived) { |         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.
 |             // 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()); |             this.storeLastReceivedMessageIfNeeded(conversationId, lastReceived, site.getId()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|                 if (excludePending) { |         if (options.excludePending) { | ||||||
|             // No need to get offline messages, return the ones we have.
 |             // No need to get offline messages, return the ones we have.
 | ||||||
|             return result; |             return result; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Get offline messages.
 |         // Get offline messages.
 | ||||||
|                 return this.messagesOffline.getConversationMessages(conversationId).then((offlineMessages) => { |         const offlineMessages = await this.messagesOffline.getConversationMessages(conversationId); | ||||||
|  | 
 | ||||||
|         // Mark offline messages as pending.
 |         // Mark offline messages as pending.
 | ||||||
|         offlineMessages.forEach((message) => { |         offlineMessages.forEach((message) => { | ||||||
|             message.pending = true; |             message.pending = true; | ||||||
|                         message.useridfrom = userId; |             message.useridfrom = options.userId; | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         result.messages = result.messages.concat(offlineMessages); |         result.messages = result.messages.concat(offlineMessages); | ||||||
| 
 | 
 | ||||||
|         return result; |         return result; | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -1412,7 +1411,7 @@ export class AddonMessagesProvider { | |||||||
|      * @param siteId Site ID. If not defined, use current site. |      * @param siteId Site ID. If not defined, use current site. | ||||||
|      * @return Promise resolved with the data. |      * @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[]> { |             toDisplay: boolean = true, siteId?: string): Promise<AddonMessagesGetMessagesMessage[]> { | ||||||
|         limitFromUnread = limitFromUnread || 0; |         limitFromUnread = limitFromUnread || 0; | ||||||
|         limitFromRead = limitFromRead || 0; |         limitFromRead = limitFromRead || 0; | ||||||
| @ -1962,7 +1961,8 @@ export class AddonMessagesProvider { | |||||||
|      * @since  3.2 |      * @since  3.2 | ||||||
|      */ |      */ | ||||||
|     isMarkAllMessagesReadEnabled(): boolean { |     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. |  * Conversation. | ||||||
|  */ |  */ | ||||||
|  | |||||||
| @ -32,8 +32,10 @@ export class AddonMessagesSettingsHandler implements CoreSettingsHandler { | |||||||
|      * |      * | ||||||
|      * @return Whether or not the handler is enabled on a site level. |      * @return Whether or not the handler is enabled on a site level. | ||||||
|      */ |      */ | ||||||
|     isEnabled(): boolean | Promise<boolean> { |     async isEnabled(): Promise<boolean> { | ||||||
|         return this.messagesProvider.isMessagePreferencesEnabled(); |         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 { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreSyncProvider } from '@providers/sync'; | import { CoreSyncProvider } from '@providers/sync'; | ||||||
|  | import { CoreConstants } from '@core/constants'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service to sync messages. |  * Service to sync messages. | ||||||
| @ -134,37 +135,44 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { | |||||||
|      * |      * | ||||||
|      * @param conversationId Conversation ID. |      * @param conversationId Conversation ID. | ||||||
|      * @param userId User ID talking to (if no 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(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         const syncId = this.getSyncId(conversationId, userId), |         const syncId = this.getSyncId(conversationId, userId); | ||||||
|             groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); |  | ||||||
| 
 | 
 | ||||||
|         if (this.isSyncing(syncId, siteId)) { |         if (this.isSyncing(syncId, siteId)) { | ||||||
|             // There's already a sync ongoing for this conversation, return the promise.
 |             // There's already a sync ongoing for this conversation, return the promise.
 | ||||||
|             return this.getOngoingSync(syncId, siteId); |             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) { |         if (conversationId) { | ||||||
|             this.logger.debug(`Try to sync conversation '${conversationId}'`); |             this.logger.debug(`Try to sync conversation '${conversationId}'`); | ||||||
|  |             messages = await this.messagesOffline.getConversationMessages(conversationId, siteId); | ||||||
|         } else { |         } else { | ||||||
|             this.logger.debug(`Try to sync discussion with user '${userId}'`); |             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 (conversationId) { |  | ||||||
|             syncPromise = this.messagesOffline.getConversationMessages(conversationId, siteId); |  | ||||||
|         } else { |  | ||||||
|             syncPromise = this.messagesOffline.getMessages(userId, siteId); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         syncPromise = syncPromise.then((messages) => { |  | ||||||
|         if (!messages.length) { |         if (!messages.length) { | ||||||
|             // Nothing to sync.
 |             // Nothing to sync.
 | ||||||
|             return []; |             return []; | ||||||
| @ -175,71 +183,117 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { | |||||||
|             return Promise.reject(null); |             return Promise.reject(null); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             let promise: Promise<any> = Promise.resolve(); |  | ||||||
|             const errors = []; |  | ||||||
| 
 |  | ||||||
|         // Order message by timecreated.
 |         // Order message by timecreated.
 | ||||||
|         messages = this.messagesProvider.sortMessages(messages); |         messages = this.messagesProvider.sortMessages(messages); | ||||||
| 
 | 
 | ||||||
|             // Send the messages.
 |         // Get messages sent by the user after the first offline message was sent.
 | ||||||
|             // Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed.
 |         // We subtract some time because the message could've been saved in server before it was in the app.
 | ||||||
|             messages.forEach((message, index) => { |         const timeFrom = Math.floor((messages[0].timecreated - CoreConstants.WS_TIMEOUT - 1000) / 1000); | ||||||
|                 // Chain message sending. If 1 message fails to be sent we'll stop sending.
 |         const onlineMessages = await this.getMessagesSentAfter(timeFrom, conversationId, userId, siteId); | ||||||
|                 promise = promise.then(() => { |  | ||||||
|                     let subPromise; |  | ||||||
| 
 | 
 | ||||||
|                     if (conversationId) { |         // Send the messages. Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed.
 | ||||||
|                         subPromise = this.messagesProvider.sendMessageToConversationOnline(conversationId, message.text, siteId); |         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]; | ||||||
|  | 
 | ||||||
|  |             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 { |                 } else { | ||||||
|                         subPromise = this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId); |                     await this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId); | ||||||
|                 } |                 } | ||||||
| 
 |             } catch (error) { | ||||||
|                     return subPromise.catch((error) => { |                 if (!this.utils.isWebServiceError(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); |  | ||||||
|                             } |  | ||||||
| 
 |  | ||||||
|                             return; |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                     // Error sending, stop execution.
 |                     // Error sending, stop execution.
 | ||||||
|                     if (this.appProvider.isOnline()) { |                     if (this.appProvider.isOnline()) { | ||||||
|                         // App is online, unmark deviceoffline if marked.
 |                         // App is online, unmark deviceoffline if marked.
 | ||||||
|                         this.messagesOffline.setMessagesDeviceOffline(messages, false); |                         this.messagesOffline.setMessagesDeviceOffline(messages, false); | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                         return Promise.reject(error); |                     throw error; | ||||||
|                     }).then(() => { |                 } | ||||||
|  | 
 | ||||||
|  |                 // Error returned by WS. Store the error to show a warning but keep sending messages.
 | ||||||
|  |                 if (errors.indexOf(error) == -1) { | ||||||
|  |                     errors.push(error); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             // Message was sent, delete it from local DB.
 |             // Message was sent, delete it from local DB.
 | ||||||
|             if (conversationId) { |             if (conversationId) { | ||||||
|                             return this.messagesOffline.deleteConversationMessage(conversationId, message.text, |                 await this.messagesOffline.deleteConversationMessage(conversationId, message.text, message.timecreated, siteId); | ||||||
|                                     message.timecreated, siteId); |  | ||||||
|             } else { |             } else { | ||||||
|                             return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); |                 await this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); | ||||||
|             } |             } | ||||||
|                     }).then(() => { | 
 | ||||||
|             // In some Moodle versions, wait 1 second to make sure timecreated is different.
 |             // 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.
 |             // This is because there was a bug where messages with the same timecreated had a wrong order.
 | ||||||
|                         if (!groupMessagingEnabled && index < messages.length - 1) { |             if (!groupMessagingEnabled && i < messages.length - 1) { | ||||||
|                             return new Promise((resolve, reject): any => { |                 await this.utils.wait(1000); | ||||||
|                                 setTimeout(resolve, 1000); |             } | ||||||
|                             }); |  | ||||||
|         } |         } | ||||||
|                     }); |  | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|             return promise; |         await this.handleSyncErrors(conversationId, userId, errors, warnings); | ||||||
|         }).then((errors) => { | 
 | ||||||
|             return this.handleSyncErrors(conversationId, userId, errors, warnings); |  | ||||||
|         }).then(() => { |  | ||||||
|         // All done, return the warnings.
 |         // All done, return the warnings.
 | ||||||
|         return 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 this.addOngoingSync(syncId, syncPromise, siteId); |                 const sentMessages = result.messages.filter((message) => message.useridfrom == siteCurrentUserId); | ||||||
|  | 
 | ||||||
|  |                 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. |      * @param warnings Array where to place the warnings. | ||||||
|      * @return Promise resolved when done. |      * @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 (errors && errors.length) { | ||||||
|             if (conversationId) { |             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]="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="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="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-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -175,7 +175,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|             this.hasOffline = hasOffline; |             this.hasOffline = hasOffline; | ||||||
| 
 | 
 | ||||||
|             // Get assignment submissions.
 |             // 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(); |                 const time = this.timeUtils.timestamp(); | ||||||
| 
 | 
 | ||||||
|                 this.canViewAllSubmissions = data.canviewsubmissions; |                 this.canViewAllSubmissions = data.canviewsubmissions; | ||||||
| @ -217,7 +217,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Check if the user can view their own submission.
 |                 // 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; |                     this.canViewOwnSubmission = true; | ||||||
|                 }).catch((error) => { |                 }).catch((error) => { | ||||||
|                     this.canViewOwnSubmission = false; |                     this.canViewOwnSubmission = false; | ||||||
| @ -241,7 +241,10 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|     setGroup(groupId: number): Promise<any> { |     setGroup(groupId: number): Promise<any> { | ||||||
|         this.group = groupId; |         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; |             this.summary = response.gradingsummary; | ||||||
|             if (typeof this.summary.warnofungroupedusers == 'boolean' && this.summary.warnofungroupedusers) { |             if (typeof this.summary.warnofungroupedusers == 'boolean' && this.summary.warnofungroupedusers) { | ||||||
|                 this.summary.warnofungroupedusers = 'ungroupedusers'; |                 this.summary.warnofungroupedusers = 'ungroupedusers'; | ||||||
| @ -299,7 +302,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|      */ |      */ | ||||||
|     protected hasSyncSucceed(result: any): boolean { |     protected hasSyncSucceed(result: any): boolean { | ||||||
|         if (result.updated) { |         if (result.updated) { | ||||||
|             this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); |             this.submissionComponent && this.submissionComponent.invalidateAndRefresh(false); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return result.updated; |         return result.updated; | ||||||
| @ -324,7 +327,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return Promise.all(promises).finally(() => { |         return Promise.all(promises).finally(() => { | ||||||
|             this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); |             this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ | |||||||
|     </ion-item> |     </ion-item> | ||||||
| 
 | 
 | ||||||
|     <!-- Tabs: see the submission or grade it. --> |     <!-- 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. --> |         <!-- View the submission tab. --> | ||||||
|         <core-tab [title]="'addon.mod_assign.submission' | translate"> |         <core-tab [title]="'addon.mod_assign.submission' | translate"> | ||||||
|             <ng-template> |             <ng-template> | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ import { Component, Input, OnInit, OnDestroy, ViewChild, Optional, ViewChildren, | |||||||
| import { NavController } from 'ionic-angular'; | import { NavController } from 'ionic-angular'; | ||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreEventsProvider } from '@providers/events'; | import { CoreEventsProvider, CoreEventObserver } from '@providers/events'; | ||||||
| import { CoreGroupsProvider } from '@providers/groups'; | import { CoreGroupsProvider } from '@providers/groups'; | ||||||
| import { CoreLangProvider } from '@providers/lang'; | import { CoreLangProvider } from '@providers/lang'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider } from '@providers/sites'; | ||||||
| @ -35,7 +35,9 @@ import { | |||||||
| } from '../../providers/assign'; | } from '../../providers/assign'; | ||||||
| import { AddonModAssignHelperProvider } from '../../providers/helper'; | import { AddonModAssignHelperProvider } from '../../providers/helper'; | ||||||
| import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; | import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; | ||||||
|  | import { AddonModAssignSync, AddonModAssignSyncProvider } from '../../providers/assign-sync'; | ||||||
| import { CoreTabsComponent } from '@components/tabs/tabs'; | import { CoreTabsComponent } from '@components/tabs/tabs'; | ||||||
|  | import { CoreTabComponent } from '@components/tabs/tab'; | ||||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||||
| import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; | 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 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 originalGrades: any = {}; // Object with the original grade data, to check for changes.
 | ||||||
|     protected isDestroyed: boolean; // Whether the component has been destroyed.
 |     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, |     constructor(protected navCtrl: NavController, protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider, | ||||||
|             sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected timeUtils: CoreTimeUtilsProvider, |             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.selectedTab = this.showGrade && this.showGrade !== 'false' ? 1 : 0; | ||||||
|         this.isSubmittedForGrading = !!this.submitId; |         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); |                     }, this.siteId); | ||||||
|                 } else { |                 } else { | ||||||
|                     // Invalidate and refresh data to update this view.
 |                     // Invalidate and refresh data to update this view.
 | ||||||
|                     this.invalidateAndRefresh(); |                     this.invalidateAndRefresh(true); | ||||||
|                 } |                 } | ||||||
|             }).catch((error) => { |             }).catch((error) => { | ||||||
|                 this.domUtils.showErrorModalDefault(error, 'core.error', true); |                 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). |      * 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. |      * @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) { |         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.
 |         // Check if numeric grade and toggles changed.
 | ||||||
|         if (this.originalGrades.grade != this.grade.grade || this.originalGrades.addAttempt != this.grade.addAttempt || |         if (this.originalGrades.grade != this.grade.grade || this.originalGrades.addAttempt != this.grade.addAttempt || | ||||||
|                 this.originalGrades.applyToAll != this.grade.applyToAll) { |                 this.originalGrades.applyToAll != this.grade.applyToAll) { | ||||||
|             return Promise.resolve(true); |             return true; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Check if outcomes changed.
 |         // Check if outcomes changed.
 | ||||||
| @ -301,20 +333,21 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|                 if (this.originalGrades.outcomes[outcome.id] == 'undefined' || |                 if (this.originalGrades.outcomes[outcome.id] == 'undefined' || | ||||||
|                         this.originalGrades.outcomes[outcome.id] != outcome.selectedId) { |                         this.originalGrades.outcomes[outcome.id] != outcome.selectedId) { | ||||||
|                     return Promise.resolve(true); |                     return true; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.feedback && this.feedback.plugins) { |         if (this.feedback && this.feedback.plugins) { | ||||||
|             return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId) |             try { | ||||||
|                     .catch(() => { |                 return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId); | ||||||
|  |             } catch (error) { | ||||||
|                 // Error ocurred, consider there are no changes.
 |                 // Error ocurred, consider there are no changes.
 | ||||||
|                 return false; |                 return false; | ||||||
|             }); |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return Promise.resolve(false); |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -334,9 +367,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
|     /** |     /** | ||||||
|      * Invalidate and refresh data. |      * Invalidate and refresh data. | ||||||
|      * |      * | ||||||
|  |      * @param sync Whether to try to synchronize data. | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     invalidateAndRefresh(): Promise<any> { |     invalidateAndRefresh(sync?: boolean): Promise<any> { | ||||||
|         this.loaded = false; |         this.loaded = false; | ||||||
| 
 | 
 | ||||||
|         const promises = []; |         const promises = []; | ||||||
| @ -361,16 +395,17 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
|         return Promise.all(promises).catch(() => { |         return Promise.all(promises).catch(() => { | ||||||
|             // Ignore errors.
 |             // Ignore errors.
 | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             return this.loadData(); |             return this.loadData(sync); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Load the data to render the submission. |      * Load the data to render the submission. | ||||||
|      * |      * | ||||||
|  |      * @param sync Whether to try to synchronize data. | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected loadData(): Promise<any> { |     protected async loadData(sync?: boolean): Promise<any> { | ||||||
|         let isBlind = !!this.blindId; |         let isBlind = !!this.blindId; | ||||||
| 
 | 
 | ||||||
|         this.previousAttempt = undefined; |         this.previousAttempt = undefined; | ||||||
| @ -381,44 +416,53 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
|             isBlind = false; |             isBlind = false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         try { | ||||||
|             // Get the assignment.
 |             // Get the assignment.
 | ||||||
|         return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { |             this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId); | ||||||
|             const time = this.timeUtils.timestamp(), |  | ||||||
|                 promises = []; |  | ||||||
| 
 | 
 | ||||||
|             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) { |                     if (result && result.updated) { | ||||||
|                 this.fromDate = this.timeUtils.userDate(assign.allowsubmissionsfromdate * 1000); |                         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.currentAttempt = 0; | ||||||
|             this.maxAttemptsText = this.translate.instant('addon.mod_assign.unlimitedattempts'); |             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) { |             if (!this.blindMarking && this.submitId != this.currentUserId) { | ||||||
|                 promises.push(this.userProvider.getProfile(this.submitId, this.courseId).then((profile) => { |                 promises.push(this.loadSubmissionUserProfile()); | ||||||
|                     this.user = profile; |  | ||||||
|                 })); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Check if there's any offline data for this submission.
 |             // Check if there's any offline data for this submission.
 | ||||||
|             promises.push(this.assignOfflineProvider.getSubmission(assign.id, this.submitId).then((data) => { |             promises.push(this.loadSubmissionOfflineData()); | ||||||
|                 this.hasOffline = data && data.plugindata && Object.keys(data.plugindata).length > 0; | 
 | ||||||
|                 this.submittedOffline = data && data.submitted; |             await Promise.all(promises); | ||||||
|             }).catch(() => { |  | ||||||
|                 // No offline data found.
 |  | ||||||
|                 this.hasOffline = false; |  | ||||||
|                 this.submittedOffline = false; |  | ||||||
|             })); |  | ||||||
| 
 | 
 | ||||||
|             return Promise.all(promises); |  | ||||||
|         }).then(() => { |  | ||||||
|             // Get submission status.
 |             // Get submission status.
 | ||||||
|             return this.assignProvider.getSubmissionStatusWithRetry(this.assign, this.submitId, undefined, isBlind); |             const response = await this.assignProvider.getSubmissionStatusWithRetry(this.assign, {userId: this.submitId, isBlind}); | ||||||
|         }).then((response) => { |  | ||||||
| 
 | 
 | ||||||
|             const promises = []; |             promises = []; | ||||||
| 
 | 
 | ||||||
|             this.submissionStatusAvailable = true; |             this.submissionStatusAvailable = true; | ||||||
|             this.lastAttempt = response.lastattempt; |             this.lastAttempt = response.lastattempt; | ||||||
| @ -450,16 +494,41 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Get the submission plugins that don't support editing.
 |             // Get the submission plugins that don't support editing.
 | ||||||
|             promises.push(this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins).then((list) => { |             promises.push(this.loadUnsupportedPlugins()); | ||||||
|                 this.unsupportedEditPlugins = list; |  | ||||||
|             })); |  | ||||||
| 
 | 
 | ||||||
|             return Promise.all(promises); |             await Promise.all(promises); | ||||||
|         }).catch((error) => { |         } catch (error) { | ||||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); |             this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); | ||||||
|         }).finally(() => { |         } finally { | ||||||
|             this.loaded = true; |             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.
 |             // Make sure outcomes is an array.
 | ||||||
|             gradeInfo.outcomes = gradeInfo.outcomes || []; |             gradeInfo.outcomes = gradeInfo.outcomes || []; | ||||||
| 
 | 
 | ||||||
|             if (!this.isDestroyed) { |  | ||||||
|                 // Block the assignment.
 |  | ||||||
|                 this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Treat the grade info.
 |             // Treat the grade info.
 | ||||||
|             return this.treatGradeInfo(); |             return this.treatGradeInfo(); | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
| @ -589,11 +653,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
|                 return this.assignOfflineProvider.getSubmissionGrade(this.assign.id, this.submitId).catch(() => { |                 return this.assignOfflineProvider.getSubmissionGrade(this.assign.id, this.submitId).catch(() => { | ||||||
|                     // Grade not found.
 |                     // Grade not found.
 | ||||||
|                 }).then((data) => { |                 }).then((data) => { | ||||||
|  |                     this.hasOfflineGrade = false; | ||||||
| 
 | 
 | ||||||
|                     // Load offline grades.
 |                     // Load offline grades.
 | ||||||
|                     if (data && (!feedback || !feedback.gradeddate || feedback.gradeddate < data.timemodified)) { |                     if (data && (!feedback || !feedback.gradeddate || feedback.gradeddate < data.timemodified)) { | ||||||
|                         // If grade has been modified from gradebook, do not use offline.
 |                         // If grade has been modified from gradebook, do not use offline.
 | ||||||
|                         if (this.grade.modified < data.timemodified) { |                         if (this.grade.modified < data.timemodified) { | ||||||
|  |                             this.hasOfflineGrade = true; | ||||||
|                             this.grade.grade = !this.grade.scale ? this.utils.formatFloat(data.grade) : data.grade; |                             this.grade.grade = !this.grade.scale ? this.utils.formatFloat(data.grade) : data.grade; | ||||||
|                             this.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced'; |                             this.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced'; | ||||||
|                             this.gradingColor = ''; |                             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. |      * Set the submission status name and class. | ||||||
|      * |      * | ||||||
| @ -725,7 +800,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
|      */ |      */ | ||||||
|     submitGrade(): Promise<any> { |     submitGrade(): Promise<any> { | ||||||
|         // Check if there's something to be saved.
 |         // Check if there's something to be saved.
 | ||||||
|         return this.hasDataToSave().then((modified) => { |         return this.hasDataToSave(true).then((modified) => { | ||||||
|             if (!modified) { |             if (!modified) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| @ -764,7 +839,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
|                     return this.discardDrafts(); |                     return this.discardDrafts(); | ||||||
|                 }).finally(() => { |                 }).finally(() => { | ||||||
|                     // Invalidate and refresh data.
 |                     // Invalidate and refresh data.
 | ||||||
|                     this.invalidateAndRefresh(); |                     this.invalidateAndRefresh(true); | ||||||
| 
 | 
 | ||||||
|                     this.eventsProvider.trigger(AddonModAssignProvider.GRADED_EVENT, { |                     this.eventsProvider.trigger(AddonModAssignProvider.GRADED_EVENT, { | ||||||
|                         assignmentId: this.assign.id, |                         assignmentId: this.assign.id, | ||||||
| @ -921,7 +996,9 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | |||||||
|                 response.lastattempt.submissiongroupmemberswhoneedtosubmit.forEach((member) => { |                 response.lastattempt.submissiongroupmemberswhoneedtosubmit.forEach((member) => { | ||||||
|                     if (this.blindMarking) { |                     if (this.blindMarking) { | ||||||
|                         // Users not blinded! (Moodle < 3.1.1, 3.2).
 |                         // 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); |                             this.membersToSubmit.push(blindId); | ||||||
|                         })); |                         })); | ||||||
|                     } else { |                     } 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. |      * Component being destroyed. | ||||||
|      */ |      */ | ||||||
|     ngOnDestroy(): void { |     ngOnDestroy(): void { | ||||||
|  |         this.setGradeSyncBlocked(false); | ||||||
|         this.isDestroyed = true; |         this.isDestroyed = true; | ||||||
| 
 |         this.syncObserver && this.syncObserver.off(); | ||||||
|         if (this.assign && this.isGrading) { |  | ||||||
|             this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -89,6 +89,7 @@ | |||||||
|     "submitassignment": "Submit assignment", |     "submitassignment": "Submit assignment", | ||||||
|     "submittedearly": "Assignment was submitted {{$a}} early", |     "submittedearly": "Assignment was submitted {{$a}} early", | ||||||
|     "submittedlate": "Assignment was submitted {{$a}} late", |     "submittedlate": "Assignment was submitted {{$a}} late", | ||||||
|  |     "syncblockedusercomponent": "user grade", | ||||||
|     "timemodified": "Last modified", |     "timemodified": "Last modified", | ||||||
|     "timeremaining": "Time remaining", |     "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.", |     "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 { IonicPage, NavController, NavParams } from 'ionic-angular'; | ||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreEventsProvider } from '@providers/events'; | import { CoreEventsProvider } from '@providers/events'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreSyncProvider } from '@providers/sync'; | import { CoreSyncProvider } from '@providers/sync'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; | import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; | ||||||
| @ -125,11 +125,20 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { | |||||||
|         }).then(() => { |         }).then(() => { | ||||||
| 
 | 
 | ||||||
|             // Get submission status. Ignore cache to get the latest data.
 |             // Get submission status. Ignore cache to get the latest data.
 | ||||||
|             return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind, false, true) |             const options = { | ||||||
|                     .catch((err) => { |                 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.
 |                 // Cannot connect. Get cached data.
 | ||||||
|                 return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind) |                 options.filter = true; | ||||||
|                         .then((response) => { |                 options.readingStrategy = CoreSitesReadingStrategy.PreferCache; | ||||||
|  | 
 | ||||||
|  |                 return this.assignProvider.getSubmissionStatus(this.assign.id, options).then((response) => { | ||||||
|                     const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt); |                     const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt); | ||||||
| 
 | 
 | ||||||
|                     // Check if the user can edit it in offline.
 |                     // Check if the user can edit it in offline.
 | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ | |||||||
| import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; | import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; | ||||||
| import { IonicPage, NavParams } from 'ionic-angular'; | import { IonicPage, NavParams } from 'ionic-angular'; | ||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreEventsProvider } from '@providers/events'; | import { CoreEventsProvider, CoreEventObserver } from '@providers/events'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | ||||||
| @ -23,6 +23,7 @@ import { | |||||||
|     AddonModAssignProvider, AddonModAssignAssign, AddonModAssignGrade, AddonModAssignSubmission |     AddonModAssignProvider, AddonModAssignAssign, AddonModAssignGrade, AddonModAssignSubmission | ||||||
| } from '../../providers/assign'; | } from '../../providers/assign'; | ||||||
| import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; | import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; | ||||||
|  | import { AddonModAssignSyncProvider, AddonModAssignSync } from '../../providers/assign-sync'; | ||||||
| import { AddonModAssignHelperProvider, AddonModAssignSubmissionFormatted } from '../../providers/helper'; | import { AddonModAssignHelperProvider, AddonModAssignSubmissionFormatted } from '../../providers/helper'; | ||||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | 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 moduleId: number; // Module ID the submission belongs to.
 | ||||||
|     protected courseId: number; // Course ID the assignment belongs to.
 |     protected courseId: number; // Course ID the assignment belongs to.
 | ||||||
|     protected selectedStatus: string; // The status to see.
 |     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[]}; |     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 domUtils: CoreDomUtilsProvider, protected translate: TranslateService, | ||||||
|             protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, |             protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, | ||||||
|             protected assignHelper: AddonModAssignHelperProvider, protected groupsProvider: CoreGroupsProvider) { |             protected assignHelper: AddonModAssignHelperProvider, protected groupsProvider: CoreGroupsProvider) { | ||||||
| @ -79,22 +81,37 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|         // Update data if some grade changes.
 |         // Update data if some grade changes.
 | ||||||
|         this.gradedObserver = eventsProvider.on(AddonModAssignProvider.GRADED_EVENT, (data) => { |         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.
 |                 // Grade changed, refresh the data.
 | ||||||
|                 this.loaded = false; |                 this.loaded = false; | ||||||
| 
 | 
 | ||||||
|                 this.refreshAllData().finally(() => { |                 this.refreshAllData(true).finally(() => { | ||||||
|                     this.loaded = true; |                     this.loaded = true; | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         }, sitesProvider.getCurrentSiteId()); |         }, 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. |      * Component being initialized. | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|         this.fetchAssignment().finally(() => { |         this.fetchAssignment(true).finally(() => { | ||||||
|             if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) { |             if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) { | ||||||
|                 // Take first and load it.
 |                 // Take first and load it.
 | ||||||
|                 this.loadSubmission(this.submissions[0]); |                 this.loadSubmission(this.submissions[0]); | ||||||
| @ -107,34 +124,49 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | |||||||
|     /** |     /** | ||||||
|      * Fetch assignment data. |      * Fetch assignment data. | ||||||
|      * |      * | ||||||
|  |      * @param sync Whether to try to synchronize data. | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected fetchAssignment(): Promise<any> { |     protected async fetchAssignment(sync?: boolean): Promise<void> { | ||||||
| 
 |         try { | ||||||
|             // Get assignment data.
 |             // Get assignment data.
 | ||||||
|         return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { |             this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId); | ||||||
|             this.title = assign.name || this.title; |  | ||||||
|             this.assign = assign; |  | ||||||
| 
 | 
 | ||||||
|             // Get assignment submissions.
 |             this.title = this.assign.name || this.title; | ||||||
|             return this.assignProvider.getSubmissions(assign.id); | 
 | ||||||
|         }).then((data) => { |             if (sync) { | ||||||
|             if (!data.canviewsubmissions) { |                 try { | ||||||
|                 // User shouldn't be able to reach here.
 |                     // Try to synchronize data.
 | ||||||
|                 return Promise.reject(null); |                     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.
 |             // Check if groupmode is enabled to avoid showing wrong numbers.
 | ||||||
|             return this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false).then((groupInfo) => { |             this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false); | ||||||
|                 this.groupInfo = groupInfo; |  | ||||||
| 
 | 
 | ||||||
|                 return this.setGroup(this.groupsProvider.validateGroupId(this.groupId, groupInfo)); |             await this.setGroup(this.groupsProvider.validateGroupId(this.groupId, this.groupInfo)); | ||||||
|             }); |         } catch (error) { | ||||||
|         }).catch((error) => { |  | ||||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); |             this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); | ||||||
|         }); |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -160,7 +192,8 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | |||||||
|         const promises = [ |         const promises = [ | ||||||
|             this.assignHelper.getSubmissionsUserData(this.assign, this.submissionsData.submissions, this.groupId), |             this.assignHelper.getSubmissionsUserData(this.assign, this.submissionsData.submissions, this.groupId), | ||||||
|             // Get assignment grades only if workflow is not enabled to check grading date.
 |             // 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[]]) => { |         return Promise.all(promises).then(([submissions, grades]: [AddonModAssignSubmissionFormatted[], AddonModAssignGrade[]]) => { | ||||||
| @ -265,9 +298,10 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | |||||||
|     /** |     /** | ||||||
|      * Refresh all the data. |      * Refresh all the data. | ||||||
|      * |      * | ||||||
|  |      * @param sync Whether to try to synchronize data. | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected refreshAllData(): Promise<any> { |     protected refreshAllData(sync?: boolean): Promise<any> { | ||||||
|         const promises = []; |         const promises = []; | ||||||
| 
 | 
 | ||||||
|         promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); |         promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); | ||||||
| @ -279,7 +313,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return Promise.all(promises).finally(() => { |         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. |      * @param refresher Refresher. | ||||||
|      */ |      */ | ||||||
|     refreshList(refresher: any): void { |     refreshList(refresher: any): void { | ||||||
|         this.refreshAllData().finally(() => { |         this.refreshAllData(true).finally(() => { | ||||||
|             refresher.complete(); |             refresher.complete(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| @ -299,6 +333,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | |||||||
|      */ |      */ | ||||||
|     ngOnDestroy(): void { |     ngOnDestroy(): void { | ||||||
|         this.gradedObserver && this.gradedObserver.off(); |         this.gradedObserver && this.gradedObserver.off(); | ||||||
|  |         this.syncObserver && this.syncObserver.off(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -137,7 +137,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return Promise.all(promises).finally(() => { |         return Promise.all(promises).finally(() => { | ||||||
|             this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); |             this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); | ||||||
| 
 | 
 | ||||||
|             return this.fetchSubmission(); |             return this.fetchSubmission(); | ||||||
|         }); |         }); | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; | |||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreEventsProvider } from '@providers/events'; | import { CoreEventsProvider } from '@providers/events'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreSyncProvider } from '@providers/sync'; | import { CoreSyncProvider } from '@providers/sync'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||||
| @ -25,12 +25,14 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; | |||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | import { CoreCourseProvider } from '@core/course/providers/course'; | ||||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; | import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; | ||||||
| import { CoreSyncBaseProvider } from '@classes/base-sync'; | import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync'; | ||||||
| import { AddonModAssignProvider, AddonModAssignAssign } from './assign'; | import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission } from './assign'; | ||||||
| import { AddonModAssignOfflineProvider } from './assign-offline'; | import { AddonModAssignOfflineProvider } from './assign-offline'; | ||||||
| import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | ||||||
| import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; | import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; | ||||||
| 
 | 
 | ||||||
|  | import { makeSingleton } from '@singletons/core.singletons'; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Data returned by an assign sync. |  * Data returned by an assign sync. | ||||||
|  */ |  */ | ||||||
| @ -44,6 +46,11 @@ export interface AddonModAssignSyncResult { | |||||||
|      * Whether data was updated in the site. |      * Whether data was updated in the site. | ||||||
|      */ |      */ | ||||||
|     updated: boolean; |     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 { | export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||||
| 
 | 
 | ||||||
|     static AUTO_SYNCED = 'addon_mod_assign_autom_synced'; |     static AUTO_SYNCED = 'addon_mod_assign_autom_synced'; | ||||||
|  |     static MANUAL_SYNCED = 'addon_mod_assign_manual_synced'; | ||||||
| 
 | 
 | ||||||
|     protected componentTranslate: string; |     protected componentTranslate: string; | ||||||
| 
 | 
 | ||||||
| @ -79,6 +87,17 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | |||||||
|         this.componentTranslate = courseProvider.translateModuleName('assign'); |         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. |      * 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. |      * @param force Wether to force sync not depending on last execution. | ||||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. |      * @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); |         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 force Wether to force sync not depending on last execution. | ||||||
|      * @param Promise resolved if sync is successful, rejected if sync fails. |      * @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.
 |         // Get all assignments that have offline data.
 | ||||||
|         return this.assignOfflineProvider.getAllAssigns(siteId).then((assignIds) => { |         const assignIds = await this.assignOfflineProvider.getAllAssigns(siteId); | ||||||
|             // Sync all assignments that haven't been synced for a while.
 | 
 | ||||||
|             const promises = assignIds.map((assignId) => { |         // Try to sync all assignments.
 | ||||||
|                 const promise = force ? this.syncAssign(assignId, siteId) : this.syncAssignIfNeeded(assignId, siteId); |         await Promise.all(assignIds.map(async (assignId) => { | ||||||
|  |             const data = force ? await this.syncAssign(assignId, siteId) : await this.syncAssignIfNeeded(assignId, siteId); | ||||||
|  | 
 | ||||||
|  |             if (!data || !data.updated) { | ||||||
|  |                 // Not updated.
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|                 return promise.then((data) => { |  | ||||||
|                     if (data && data.updated) { |  | ||||||
|                         // Sync done. Send event.
 |  | ||||||
|             this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, { |             this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, { | ||||||
|                 assignId: assignId, |                 assignId: assignId, | ||||||
|                             warnings: data.warnings |                 warnings: data.warnings, | ||||||
|  |                 gradesBlocked: data.gradesBlocked, | ||||||
|             }, siteId); |             }, siteId); | ||||||
|                     } |         })); | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             return Promise.all(promises); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -161,12 +179,12 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @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. |      * @return Promise resolved when the assign is synced or it doesn't need to be synced. | ||||||
|      */ |      */ | ||||||
|     syncAssignIfNeeded(assignId: number, siteId?: string): Promise<void | AddonModAssignSyncResult> { |     async syncAssignIfNeeded(assignId: number, siteId?: string): Promise<void | AddonModAssignSyncResult> { | ||||||
|         return this.isSyncNeeded(assignId, siteId).then((needed) => { |         const needed = await this.isSyncNeeded(assignId, siteId); | ||||||
|  | 
 | ||||||
|         if (needed) { |         if (needed) { | ||||||
|             return this.syncAssign(assignId, siteId); |             return this.syncAssign(assignId, siteId); | ||||||
|         } |         } | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -176,18 +194,9 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved in success. |      * @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(); |         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)) { |         if (this.isSyncing(assignId, siteId)) { | ||||||
|             // There's already a sync ongoing for this assign, return the promise.
 |             // There's already a sync ongoing for this assign, return the promise.
 | ||||||
|             return this.getOngoingSync(assignId, siteId); |             return this.getOngoingSync(assignId, siteId); | ||||||
| @ -195,79 +204,126 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | |||||||
| 
 | 
 | ||||||
|         // Verify that assign isn't blocked.
 |         // Verify that assign isn't blocked.
 | ||||||
|         if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) { |         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); |         this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId); | ||||||
| 
 | 
 | ||||||
|         // Get offline submissions to be sent.
 |         const result: AddonModAssignSyncResult = { | ||||||
|         promises.push(this.assignOfflineProvider.getAssignSubmissions(assignId, siteId).catch(() => { |             warnings: [], | ||||||
|             // No offline data found, return empty array.
 |             updated: false, | ||||||
|             return []; |             gradesBlocked: [], | ||||||
|         })); |         }; | ||||||
| 
 | 
 | ||||||
|         // Get offline submission grades to be sent.
 |         // Load offline data and sync offline logs.
 | ||||||
|         promises.push(this.assignOfflineProvider.getAssignSubmissionsGrade(assignId, siteId).catch(() => { |         const promisesResults = await Promise.all([ | ||||||
|             // No offline data found, return empty array.
 |             this.getOfflineSubmissions(assignId, siteId), | ||||||
|             return []; |             this.getOfflineGrades(assignId, siteId), | ||||||
|         })); |             this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId), | ||||||
|  |         ]); | ||||||
| 
 | 
 | ||||||
|         // Sync offline logs.
 |         const submissions = promisesResults[0]; | ||||||
|         promises.push(this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId)); |         const grades = promisesResults[1]; | ||||||
| 
 |  | ||||||
|         syncPromise = Promise.all(promises).then((results) => { |  | ||||||
|             const submissions = results[0], |  | ||||||
|                 grades = results[1]; |  | ||||||
| 
 | 
 | ||||||
|         if (!submissions.length && !grades.length) { |         if (!submissions.length && !grades.length) { | ||||||
|             // Nothing to sync.
 |             // Nothing to sync.
 | ||||||
|                 return; |             await this.utils.ignoreErrors(this.setSyncTime(assignId, siteId)); | ||||||
|  | 
 | ||||||
|  |             return result; | ||||||
|         } else if (!this.appProvider.isOnline()) { |         } else if (!this.appProvider.isOnline()) { | ||||||
|             // Cannot sync in offline.
 |             // Cannot sync in offline.
 | ||||||
|                 return Promise.reject(null); |             throw new Error(this.translate.instant('core.cannotconnect')); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; |         const courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; | ||||||
| 
 | 
 | ||||||
|             return this.assignProvider.getAssignmentById(courseId, assignId, false, siteId).then((assignData) => { |         const assign = await this.assignProvider.getAssignmentById(courseId, assignId, {siteId}); | ||||||
|                 assign = assignData; |  | ||||||
| 
 | 
 | ||||||
|                 const promises = []; |         let promises = []; | ||||||
|  | 
 | ||||||
|  |         promises = promises.concat(submissions.map(async (submission) => { | ||||||
|  |             await this.syncSubmission(assign, submission, result.warnings, siteId); | ||||||
| 
 | 
 | ||||||
|                 submissions.forEach((submission) => { |  | ||||||
|                     promises.push(this.syncSubmission(assign, submission, result.warnings, siteId).then(() => { |  | ||||||
|             result.updated = true; |             result.updated = true; | ||||||
|         })); |         })); | ||||||
|                 }); |  | ||||||
| 
 | 
 | ||||||
|                 grades.forEach((grade) => { |         promises = promises.concat(grades.map(async (grade) => { | ||||||
|                     promises.push(this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId).then(() => { |             try { | ||||||
|  |                 await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId); | ||||||
|  | 
 | ||||||
|                 result.updated = true; |                 result.updated = true; | ||||||
|  |             } catch (error) { | ||||||
|  |                 if (error instanceof CoreSyncBlockedError) { | ||||||
|  |                     // Grade blocked, but allow finish the sync.
 | ||||||
|  |                     result.gradesBlocked.push(grade.userid); | ||||||
|  |                 } else { | ||||||
|  |                     throw error; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         })); |         })); | ||||||
|                 }); |  | ||||||
| 
 | 
 | ||||||
|                 return Promise.all(promises); |         await Promise.all(promises); | ||||||
|             }).then(() => { | 
 | ||||||
|         if (result.updated) { |         if (result.updated) { | ||||||
|             // Data has been sent to server. Now invalidate the WS calls.
 |             // Data has been sent to server. Now invalidate the WS calls.
 | ||||||
|                     return this.assignProvider.invalidateContent(assign.cmid, courseId, siteId).catch(() => { |             await this.utils.ignoreErrors(this.assignProvider.invalidateContent(assign.cmid, courseId, siteId)); | ||||||
|                         // Ignore errors.
 |  | ||||||
|                     }); |  | ||||||
|         } |         } | ||||||
|             }); | 
 | ||||||
|         }).then(() => { |  | ||||||
|         // Sync finished, set sync time.
 |         // Sync finished, set sync time.
 | ||||||
|             return this.setSyncTime(assignId, siteId).catch(() => { |         await this.utils.ignoreErrors(this.setSyncTime(assignId, siteId)); | ||||||
|                 // Ignore errors.
 | 
 | ||||||
|             }); |  | ||||||
|         }).then(() => { |  | ||||||
|         // All done, return the result.
 |         // All done, return the result.
 | ||||||
|         return result; |         return result; | ||||||
|         }); |     } | ||||||
| 
 | 
 | ||||||
|         return this.addOngoingSync(assignId, syncPromise, siteId); |     /** | ||||||
|  |      * 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. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved if success, rejected otherwise. |      * @return Promise resolved if success, rejected otherwise. | ||||||
|      */ |      */ | ||||||
|     protected syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string): Promise<any> { |     protected async syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string) | ||||||
|         const userId = offlineData.userid, |             : Promise<void> { | ||||||
|             pluginData = {}; |  | ||||||
|         let discardError, |  | ||||||
|             submission; |  | ||||||
| 
 | 
 | ||||||
|         return this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId).then((status) => { |         const userId = offlineData.userid; | ||||||
|             const promises = []; |         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); | ||||||
|  | 
 | ||||||
|  |         const submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt); | ||||||
| 
 | 
 | ||||||
|         if (submission.timemodified != offlineData.onlinetimemodified) { |         if (submission.timemodified != offlineData.onlinetimemodified) { | ||||||
|             // The submission was modified in Moodle, discard the submission.
 |             // The submission was modified in Moodle, discard the submission.
 | ||||||
|                 discardError = this.translate.instant('addon.mod_assign.warningsubmissionmodified'); |             this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, | ||||||
|  |                     this.translate.instant('addon.mod_assign.warningsubmissionmodified')); | ||||||
| 
 | 
 | ||||||
|                 return; |             return this.deleteSubmissionData(assign, submission, offlineData, siteId); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             submission.plugins.forEach((plugin) => { |         try { | ||||||
|                 promises.push(this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData, |             // Prepare plugins data.
 | ||||||
|                         siteId)); |             await Promise.all(submission.plugins.map(async (plugin) => { | ||||||
|             }); |                 await this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData, siteId); | ||||||
|  |             })); | ||||||
| 
 | 
 | ||||||
|             return Promise.all(promises).then(() => { |  | ||||||
|             // Now save the submission.
 |             // Now save the submission.
 | ||||||
|                 let promise; |             if (Object.keys(pluginData).length > 0) { | ||||||
| 
 |                 await this.assignProvider.saveSubmissionOnline(assign.id, pluginData, siteId); | ||||||
|                 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) { |             if (assign.submissiondrafts && offlineData.submitted) { | ||||||
|                 // The user submitted the assign manually. Submit it for grading.
 |                 // The user submitted the assign manually. Submit it for grading.
 | ||||||
|                         return this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionstatement, siteId); |                 await this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionstatement, siteId); | ||||||
|             } |             } | ||||||
|                 }).then(() => { | 
 | ||||||
|             // Submission data sent, update cached data. No need to block the user for this.
 |             // 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); |             this.assignProvider.getSubmissionStatus(assign.id, options); | ||||||
|                 }); |         } catch (error) { | ||||||
|             }).catch((error) => { |             if (!error || !this.utils.isWebServiceError(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.
 |             // A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
 | ||||||
|                     discardError = this.textUtils.getErrorMessageFromError(error); |             this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, | ||||||
|                 } else { |                 this.textUtils.getErrorMessageFromError(error)); | ||||||
|                     // Couldn't connect to server, reject.
 |  | ||||||
|                     return Promise.reject(error); |  | ||||||
|         } |         } | ||||||
|             }); | 
 | ||||||
|         }).then(() => { |  | ||||||
|         // Delete the offline data.
 |         // Delete the offline data.
 | ||||||
|             return this.assignOfflineProvider.deleteSubmission(assign.id, userId, siteId).then(() => { |         await this.deleteSubmissionData(assign, submission, offlineData, siteId); | ||||||
|                 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); |  | ||||||
|     } |     } | ||||||
|             } | 
 | ||||||
|         }); |     /** | ||||||
|  |      * 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,25 +423,42 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved if success, rejected otherwise. |      * @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> { |             siteId?: string): Promise<any> { | ||||||
| 
 | 
 | ||||||
|         const userId = offlineData.userid; |         const userId = offlineData.userid; | ||||||
|         let discardError; |         const syncId = this.getGradeSyncId(assign.id, userId); | ||||||
|  |         const options = { | ||||||
|  |             userId, | ||||||
|  |             cmId: assign.cmid, | ||||||
|  |             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||||
|  |             siteId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // 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.!!!!`); | ||||||
|  | 
 | ||||||
|  |             throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked', | ||||||
|  |                     {$a: this.translate.instant('addon.mod_assign.syncblockedusercomponent')})); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const status = await this.assignProvider.getSubmissionStatus(assign.id, options); | ||||||
| 
 | 
 | ||||||
|         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); |         const timemodified = status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified); | ||||||
| 
 | 
 | ||||||
|         if (timemodified > offlineData.timemodified) { |         if (timemodified > offlineData.timemodified) { | ||||||
|             // The submission grade was modified in Moodle, discard it.
 |             // The submission grade was modified in Moodle, discard it.
 | ||||||
|                 discardError = this.translate.instant('addon.mod_assign.warningsubmissiongrademodified'); |             this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, | ||||||
|  |                     this.translate.instant('addon.mod_assign.warningsubmissiongrademodified')); | ||||||
| 
 | 
 | ||||||
|                 return; |             return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // If grade has been modified from gradebook, do not use offline.
 |         // If grade has been modified from gradebook, do not use offline.
 | ||||||
|             return this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true).then((grades) => { |         const grades = await this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true); | ||||||
|                 return this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId).then((gradeInfo) => { | 
 | ||||||
|  |         const gradeInfo = await this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId); | ||||||
| 
 | 
 | ||||||
|         // Override offline grade and outcomes based on the gradebook data.
 |         // Override offline grade and outcomes based on the gradebook data.
 | ||||||
|         grades.forEach((grade) => { |         grades.forEach((grade) => { | ||||||
| @ -407,14 +479,14 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|                 }); | 
 | ||||||
|             }).then(() => { |         try { | ||||||
|              // Now submit the grade.
 |              // Now submit the grade.
 | ||||||
|                 return this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptnumber, |             await this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptnumber, | ||||||
|                     offlineData.addattempt, offlineData.workflowstate, offlineData.applytoall, offlineData.outcomes, |                     offlineData.addattempt, offlineData.workflowstate, offlineData.applytoall, offlineData.outcomes, | ||||||
|                         offlineData.plugindata, siteId).then(() => { |                     offlineData.plugindata, siteId); | ||||||
|                     // Grades sent.
 | 
 | ||||||
|                     // Discard grades drafts.
 |             // Grades sent. Discard grades drafts.
 | ||||||
|             const promises = []; |             const promises = []; | ||||||
|             if (status.feedback && status.feedback.plugins) { |             if (status.feedback && status.feedback.plugins) { | ||||||
|                 status.feedback.plugins.forEach((plugin) => { |                 status.feedback.plugins.forEach((plugin) => { | ||||||
| @ -423,35 +495,23 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Update cached data.
 |             // Update cached data.
 | ||||||
|                     promises.push(this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId)); |             promises.push(this.assignProvider.getSubmissionStatus(assign.id, options)); | ||||||
| 
 | 
 | ||||||
|                     return Promise.all(promises); |             await Promise.all(promises); | ||||||
|                 }).catch((error) => { |         } catch (error) { | ||||||
|                     if (error && this.utils.isWebServiceError(error)) { |             if (!error || !this.utils.isWebServiceError(error)) { | ||||||
|                         // The WebService has thrown an error, this means it cannot be submitted. Discard the offline data.
 |                 // Local error, reject.
 | ||||||
|                         discardError = this.textUtils.getErrorMessageFromError(error); |                 throw error; | ||||||
|                     } else { |  | ||||||
|                         // Couldn't connect to server, reject.
 |  | ||||||
|                     return Promise.reject(error); |  | ||||||
|             } |             } | ||||||
|                 }); | 
 | ||||||
|             }); |             // A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
 | ||||||
|         }).then(() => { |             this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, | ||||||
|  |                 this.textUtils.getErrorMessageFromError(error)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // Delete the offline data.
 |         // Delete the offline data.
 | ||||||
|             return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId); |         await 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); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export class AddonModAssignSync extends makeSingleton(AddonModAssignSyncProvider) {} | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; | |||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | 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 { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | ||||||
| import { AddonModAssignOfflineProvider } from './assign-offline'; | import { AddonModAssignOfflineProvider } from './assign-offline'; | ||||||
| import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | import { CoreSite } from '@classes/site'; | ||||||
| import { CoreInterceptor } from '@classes/interceptor'; | import { CoreInterceptor } from '@classes/interceptor'; | ||||||
| import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; | import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; | ||||||
|  | import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service that provides some functions for assign. |  * Service that provides some functions for assign. | ||||||
| @ -143,12 +144,11 @@ export class AddonModAssignProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID the assignment belongs to. |      * @param courseId Course ID the assignment belongs to. | ||||||
|      * @param cmId Assignment module ID. |      * @param cmId Assignment module ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with the assignment. |      * @return Promise resolved with the assignment. | ||||||
|      */ |      */ | ||||||
|     getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignAssign> { |     getAssignment(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModAssignAssign> { | ||||||
|         return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId); |         return this.getAssignmentByField(courseId, 'cmid', cmId, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -157,27 +157,23 @@ export class AddonModAssignProvider { | |||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param key Name of the property to check. |      * @param key Name of the property to check. | ||||||
|      * @param value Value to search. |      * @param value Value to search. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved when the assignment is retrieved. |      * @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> { |             : Promise<AddonModAssignAssign> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 courseids: [courseId], |                 courseids: [courseId], | ||||||
|                     includenotenrolledcourses: 1 |                 includenotenrolledcourses: 1, | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getAssignmentCacheKey(courseId), |                 cacheKey: this.getAssignmentCacheKey(courseId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_RARELY |                 updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||||
|  |                 component: AddonModAssignProvider.COMPONENT, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 |  | ||||||
|             if (ignoreCache) { |  | ||||||
|                 preSets.getFromCache = false; |  | ||||||
|                 preSets.emergencyCache = false; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_assign_get_assignments', params, preSets).catch(() => { |             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.
 |                 // 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. |      * Get an assignment by instance ID. | ||||||
|      * |      * | ||||||
|      * @param courseId Course ID the assignment belongs to. |      * @param courseId Course ID the assignment belongs to. | ||||||
|      * @param cmId Assignment instance ID. |      * @param id Assignment instance ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with the assignment. |      * @return Promise resolved with the assignment. | ||||||
|      */ |      */ | ||||||
|     getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignAssign> { |     getAssignmentById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModAssignAssign> { | ||||||
|         return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId); |         return this.getAssignmentByField(courseId, 'id', id, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -230,24 +225,22 @@ export class AddonModAssignProvider { | |||||||
|      * |      * | ||||||
|      * @param assignId Assignment Id. |      * @param assignId Assignment Id. | ||||||
|      * @param userId User Id to be blinded. |      * @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 options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with the user blind id. |      * @return Promise resolved with the user blind id. | ||||||
|      */ |      */ | ||||||
|     getAssignmentUserMappings(assignId: number, userId: number, ignoreCache?: boolean, siteId?: string): Promise<number> { |     getAssignmentUserMappings(assignId: number, userId: number, options: CoreCourseCommonModWSOptions = {}): Promise<number> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |  | ||||||
|             const params = { |  | ||||||
|                     assignmentids: [assignId] |  | ||||||
|                 }, |  | ||||||
|                 preSets: CoreSiteWSPreSets = { |  | ||||||
|                     cacheKey: this.getAssignmentUserMappingsCacheKey(assignId), |  | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_OFTEN |  | ||||||
|                 }; |  | ||||||
| 
 | 
 | ||||||
|             if (ignoreCache) { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|                 preSets.getFromCache = false; |             const params = { | ||||||
|                 preSets.emergencyCache = false; |                 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) |             return site.read('mod_assign_get_user_mappings', params, preSets) | ||||||
|                     .then((response: AddonModAssignGetUserMappingsResult): any => { |                     .then((response: AddonModAssignGetUserMappingsResult): any => { | ||||||
| @ -293,23 +286,21 @@ export class AddonModAssignProvider { | |||||||
|      * Returns grade information from assign_grades for the requested assignment id |      * Returns grade information from assign_grades for the requested assignment id | ||||||
|      * |      * | ||||||
|      * @param assignId 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 options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Resolved with requested info when done. |      * @return Resolved with requested info when done. | ||||||
|      */ |      */ | ||||||
|     getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignGrade[]> { |     getAssignmentGrades(assignId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModAssignGrade[]> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |  | ||||||
|             const params = { |  | ||||||
|                     assignmentids: [assignId] |  | ||||||
|                 }, |  | ||||||
|                 preSets: CoreSiteWSPreSets = { |  | ||||||
|                     cacheKey: this.getAssignmentGradesCacheKey(assignId) |  | ||||||
|                 }; |  | ||||||
| 
 | 
 | ||||||
|             if (ignoreCache) { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|                 preSets.getFromCache = false; |             const params = { | ||||||
|                 preSets.emergencyCache = false; |                 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 => { |             return site.read('mod_assign_get_grades', params, preSets).then((response: AddonModAssignGetGradesResult): any => { | ||||||
|                 // Search the assignment.
 |                 // Search the assignment.
 | ||||||
| @ -455,26 +446,23 @@ export class AddonModAssignProvider { | |||||||
|      * Get an assignment submissions. |      * Get an assignment submissions. | ||||||
|      * |      * | ||||||
|      * @param assignId 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 options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     getSubmissions(assignId: number, ignoreCache?: boolean, siteId?: string) |     getSubmissions(assignId: number, options: CoreCourseCommonModWSOptions = {}) | ||||||
|             : Promise<{canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}> { |             : Promise<{canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     assignmentids: [assignId] |                 assignmentids: [assignId], | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getSubmissionsCacheKey(assignId), |                 cacheKey: this.getSubmissionsCacheKey(assignId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_OFTEN |                 updateFrequency: CoreSite.FREQUENCY_OFTEN, | ||||||
|  |                 component: AddonModAssignProvider.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_assign_get_submissions', params, preSets) |             return site.read('mod_assign_get_submissions', params, preSets) | ||||||
|                     .then((response: AddonModAssignGetSubmissionsResult): any => { |                     .then((response: AddonModAssignGetSubmissionsResult): any => { | ||||||
| @ -510,46 +498,40 @@ export class AddonModAssignProvider { | |||||||
|      * Get information about an assignment submission status for a given user. |      * Get information about an assignment submission status for a given user. | ||||||
|      * |      * | ||||||
|      * @param assignId Assignment instance id. |      * @param assignId Assignment instance id. | ||||||
|      * @param userId User Id (empty for current user). |      * @param options Other options. | ||||||
|      * @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). |  | ||||||
|      * @return Promise always resolved with the user submission status. |      * @return Promise always resolved with the user submission status. | ||||||
|      */ |      */ | ||||||
|     getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, |     getSubmissionStatus(assignId: number, options: AddonModAssignSubmissionStatusOptions = {}) | ||||||
|             ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignGetSubmissionStatusResult> { |             : Promise<AddonModAssignGetSubmissionStatusResult> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         if (options.filter === undefined || options.filter === null) { | ||||||
|             const fixedParams = this.fixSubmissionStatusParams(site, userId, groupId, isBlind); |             options.filter = true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|  |             const fixedParams = this.fixSubmissionStatusParams(site, options.userId, options.groupId, options.isBlind); | ||||||
| 
 | 
 | ||||||
|             const params = { |             const params = { | ||||||
|                 assignid: assignId, |                 assignid: assignId, | ||||||
|                     userid: fixedParams.userId |                 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 |  | ||||||
|             }; |             }; | ||||||
| 
 |  | ||||||
|             if (fixedParams.groupId) { |             if (fixedParams.groupId) { | ||||||
|                 params['groupid'] = fixedParams.groupId; |                 params['groupid'] = fixedParams.groupId; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (ignoreCache) { |             const preSets = { | ||||||
|                 preSets.getFromCache = false; |                 cacheKey: this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId, | ||||||
|                 preSets.emergencyCache = false; |                         fixedParams.isBlind), | ||||||
|             } |                 getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account.
 | ||||||
| 
 |                 filter: options.filter, | ||||||
|             if (!filter) { |                 rewriteurls: options.filter, | ||||||
|  |                 component: AddonModAssignProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|                 // Don't cache when getting text without filters.
 |                 // Don't cache when getting text without filters.
 | ||||||
|                 // @todo Change this to support offline editing.
 |                 // @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); |             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. |      * If the data doesn't include the user submission, retry ignoring cache. | ||||||
|      * |      * | ||||||
|      * @param assign Assignment. |      * @param assign Assignment. | ||||||
|      * @param userId User id (empty for current user). |      * @param options Other options. | ||||||
|      * @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). |  | ||||||
|      * @return Promise always resolved with the user submission status. |      * @return Promise always resolved with the user submission status. | ||||||
|      */ |      */ | ||||||
|     getSubmissionStatusWithRetry(assign: any, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, |     getSubmissionStatusWithRetry(assign: any, options: AddonModAssignSubmissionStatusOptions = {}) | ||||||
|             ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignGetSubmissionStatusResult> { |             : 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); |             const userSubmission = this.getSubmissionObjectFromAttempt(assign, response.lastattempt); | ||||||
| 
 | 
 | ||||||
|             if (!userSubmission) { |             if (!userSubmission) { | ||||||
|                 // Try again, ignoring cache.
 |                 // 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.
 |                     // Error, return the first result even if it doesn't have the user submission.
 | ||||||
|                     return response; |                     return response; | ||||||
|                 }); |                 }); | ||||||
| @ -650,16 +633,15 @@ export class AddonModAssignProvider { | |||||||
|      * |      * | ||||||
|      * @param assignId Assignment id. |      * @param assignId Assignment id. | ||||||
|      * @param groupId Group id. If not defined, 0. |      * @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 options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with the list of participants and summary of submissions. |      * @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[]> { |             : Promise<AddonModAssignParticipant[]> { | ||||||
| 
 | 
 | ||||||
|         groupId = groupId || 0; |         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')) { |             if (!site.wsAvailable('mod_assign_list_participants')) { | ||||||
|                 // Silently fail if is not available. (needs Moodle version >= 3.2)
 |                 // Silently fail if is not available. (needs Moodle version >= 3.2)
 | ||||||
|                 return Promise.reject(null); |                 return Promise.reject(null); | ||||||
| @ -668,17 +650,15 @@ export class AddonModAssignProvider { | |||||||
|             const params = { |             const params = { | ||||||
|                 assignid: assignId, |                 assignid: assignId, | ||||||
|                 groupid: groupId, |                 groupid: groupId, | ||||||
|                     filter: '' |                 filter: '', | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.listParticipantsCacheKey(assignId, groupId), |                 cacheKey: this.listParticipantsCacheKey(assignId, groupId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_OFTEN |                 updateFrequency: CoreSite.FREQUENCY_OFTEN, | ||||||
|  |                 component: AddonModAssignProvider.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_assign_list_participants', params, 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> { |     invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> { | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         return this.getAssignment(courseId, moduleId, false, siteId).then((assign) => { |         return this.getAssignment(courseId, moduleId, {siteId}).then((assign) => { | ||||||
|             const promises = []; |             const promises = []; | ||||||
| 
 | 
 | ||||||
|             // Do not invalidate assignment data before getting assignment info, we need it!
 |             // 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.
 |         // 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) { |             if (!response.feedback || !response.feedback.gradeddate) { | ||||||
|                 // Not graded.
 |                 // Not graded.
 | ||||||
|                 return true; |                 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. |  * Assign data returned by mod_assign_get_assignments. | ||||||
|  */ |  */ | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; | |||||||
| import { CoreFileProvider } from '@providers/file'; | import { CoreFileProvider } from '@providers/file'; | ||||||
| import { CoreGroupsProvider } from '@providers/groups'; | import { CoreGroupsProvider } from '@providers/groups'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | ||||||
| import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; | import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; | ||||||
| @ -209,29 +209,29 @@ export class AddonModAssignHelperProvider { | |||||||
|      * |      * | ||||||
|      * @param assign Assignment object. |      * @param assign Assignment object. | ||||||
|      * @param groupId Group Id. |      * @param groupId Group Id. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with the list of participants and summary of submissions. |      * @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[]> { |             : Promise<AddonModAssignParticipant[]> { | ||||||
| 
 | 
 | ||||||
|         groupId = groupId || 0; |         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) { |             if (groupId || participants && participants.length > 0) { | ||||||
|                 return participants; |                 return participants; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // If no participants returned and all groups specified, get participants by groups.
 |             // 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 = [], |                 const promises = [], | ||||||
|                     participants: {[id: number]: AddonModAssignParticipant} = {}; |                     participants: {[id: number]: AddonModAssignParticipant} = {}; | ||||||
| 
 | 
 | ||||||
|                 info.groups.forEach((userGroup) => { |                 info.groups.forEach((userGroup) => { | ||||||
|                     promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, ignoreCache, siteId) |                     promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, modOptions).then((parts) => { | ||||||
|                             .then((parts) => { |  | ||||||
|                         // Do not get repeated users.
 |                         // Do not get repeated users.
 | ||||||
|                         parts.forEach((participant) => { |                         parts.forEach((participant) => { | ||||||
|                             participants[participant.id] = participant; |                             participants[participant.id] = participant; | ||||||
| @ -355,14 +355,15 @@ export class AddonModAssignHelperProvider { | |||||||
|      * @param assign Assignment object. |      * @param assign Assignment object. | ||||||
|      * @param submissions Submissions to get the data for. |      * @param submissions Submissions to get the data for. | ||||||
|      * @param groupId Group Id. |      * @param groupId Group Id. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site id (empty for current site). |  | ||||||
|      * @return Promise always resolved. Resolve param is the formatted submissions. |      * @return Promise always resolved. Resolve param is the formatted submissions. | ||||||
|      */ |      */ | ||||||
|     getSubmissionsUserData(assign: AddonModAssignAssign, submissions: AddonModAssignSubmissionFormatted[], groupId?: number, |     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 blind = assign.blindmarking && !assign.revealidentities; | ||||||
|             const promises = []; |             const promises = []; | ||||||
|             const result: AddonModAssignSubmissionFormatted[] = []; |             const result: AddonModAssignSubmissionFormatted[] = []; | ||||||
| @ -399,8 +400,8 @@ export class AddonModAssignHelperProvider { | |||||||
|                     // Blind but not blinded! (Moodle < 3.1.1, 3.2).
 |                     // Blind but not blinded! (Moodle < 3.1.1, 3.2).
 | ||||||
|                     delete submission.userid; |                     delete submission.userid; | ||||||
| 
 | 
 | ||||||
|                     promise = this.assignProvider.getAssignmentUserMappings(assign.id, submission.submitid, ignoreCache, siteId). |                     promise = this.assignProvider.getAssignmentUserMappings(assign.id, submission.submitid, modOptions) | ||||||
|                             then((blindId) => { |                             .then((blindId) => { | ||||||
|                         submission.blindid = blindId; |                         submission.blindid = blindId; | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; | |||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreGroupsProvider } from '@providers/groups'; | import { CoreGroupsProvider } from '@providers/groups'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| @ -80,13 +80,13 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | |||||||
|     canUseCheckUpdates(module: any, courseId: number): boolean | Promise<boolean> { |     canUseCheckUpdates(module: any, courseId: number): boolean | Promise<boolean> { | ||||||
|         // Teachers cannot use the WS because it doesn't check student submissions.
 |         // Teachers cannot use the WS because it doesn't check student submissions.
 | ||||||
|         return this.assignProvider.getAssignment(courseId, module.id).then((assign) => { |         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) { |                 if (data.canviewsubmissions) { | ||||||
|                     return false; |                     return false; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Check if the user can view their own submission.
 |                 // 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; |                     return true; | ||||||
|                 }); |                 }); | ||||||
|             }); |             }); | ||||||
| @ -108,18 +108,18 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | |||||||
| 
 | 
 | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         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.
 |             // Get intro files and attachments.
 | ||||||
|             let files = assign.introattachments || []; |             let files = assign.introattachments || []; | ||||||
|             files = files.concat(this.getIntroFilesFromInstance(module, assign)); |             files = files.concat(this.getIntroFilesFromInstance(module, assign)); | ||||||
| 
 | 
 | ||||||
|             // Now get the files in the submissions.
 |             // 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; |                 const blindMarking = assign.blindmarking && !assign.revealidentities; | ||||||
| 
 | 
 | ||||||
|                 if (data.canviewsubmissions) { |                 if (data.canviewsubmissions) { | ||||||
|                     // Teacher, get all submissions.
 |                     // 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[]) => { |                             .then((submissions: AddonModAssignSubmissionFormatted[]) => { | ||||||
| 
 | 
 | ||||||
|                         const promises = []; |                         const promises = []; | ||||||
| @ -172,8 +172,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | |||||||
|     protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string) |     protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string) | ||||||
|             : Promise<any[]> { |             : Promise<any[]> { | ||||||
| 
 | 
 | ||||||
|         return this.assignProvider.getSubmissionStatusWithRetry(assign, submitId, undefined, blindMarking, true, false, siteId) |         return this.assignProvider.getSubmissionStatusWithRetry(assign, { | ||||||
|                 .then((response) => { |             userId: submitId, | ||||||
|  |             isBlind: blindMarking, | ||||||
|  |             siteId, | ||||||
|  |         }).then((response) => { | ||||||
|             const promises = []; |             const promises = []; | ||||||
|             let userSubmission: AddonModAssignSubmission; |             let userSubmission: AddonModAssignSubmission; | ||||||
| 
 | 
 | ||||||
| @ -261,20 +264,24 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected prefetchAssign(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { |     protected prefetchAssign(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { | ||||||
|         const userId = this.sitesProvider.getCurrentSiteUserId(), |         const userId = this.sitesProvider.getCurrentSiteUserId(); | ||||||
|             promises = []; |         const promises = []; | ||||||
| 
 | 
 | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|  |         const options = { | ||||||
|  |             cmId: module.id, | ||||||
|  |             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||||
|  |             siteId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|         // Get assignment to retrieve all its submissions.
 |         // 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 = [], |             const subPromises = [], | ||||||
|                 blindMarking = assign.blindmarking && !assign.revealidentities; |                 blindMarking = assign.blindmarking && !assign.revealidentities; | ||||||
| 
 | 
 | ||||||
|             if (blindMarking) { |             if (blindMarking) { | ||||||
|                 subPromises.push(this.assignProvider.getAssignmentUserMappings(assign.id, undefined, true, siteId).catch(() => { |                 subPromises.push(this.utils.ignoreErrors(this.assignProvider.getAssignmentUserMappings(assign.id, -1, options))); | ||||||
|                     // Ignore errors.
 |  | ||||||
|                 })); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             subPromises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId)); |             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. |      * @return Promise resolved when prefetched, rejected otherwise. | ||||||
|      */ |      */ | ||||||
|     protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise<any> { |     protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise<any> { | ||||||
|  |         const options = { | ||||||
|  |             cmId: moduleId, | ||||||
|  |             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||||
|  |             siteId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|         // Get submissions.
 |         // Get submissions.
 | ||||||
|         return this.assignProvider.getSubmissions(assign.id, true, siteId).then((data) => { |         return this.assignProvider.getSubmissions(assign.id, options).then((data) => { | ||||||
|             const promises = []; |             const promises = []; | ||||||
| 
 | 
 | ||||||
|             promises.push(this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((groupInfo) => { |             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) => { |                     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[]) => { |                                 .then((submissions: AddonModAssignSubmissionFormatted[]) => { | ||||||
| 
 | 
 | ||||||
|                             const subPromises = []; |                             const subPromises = []; | ||||||
| 
 | 
 | ||||||
|                             submissions.forEach((submission) => { |                             submissions.forEach((submission) => { | ||||||
|                                 subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submission.submitid, |                                 const submissionOptions = { | ||||||
|                                         group.id, !!submission.blindid, true, true, siteId).then((subm) => { |                                     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); |                                     return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId); | ||||||
|                                 }).catch((error) => { |                                 }).catch((error) => { | ||||||
|                                     if (error && error.errorcode == 'nopermission') { |                                     if (error && error.errorcode == 'nopermission') { | ||||||
| @ -338,14 +359,21 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | |||||||
| 
 | 
 | ||||||
|                             if (!assign.markingworkflow) { |                             if (!assign.markingworkflow) { | ||||||
|                                 // Get assignment grades only if workflow is not enabled to check grading date.
 |                                 // 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.
 |                             // Prefetch the submission of the current user even if it does not exist, this will be create it.
 | ||||||
|                             if (!data.submissions || |                             if (!data.submissions || | ||||||
|                                     !data.submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) { |                                     !data.submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) { | ||||||
|                                 subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, userId, group.id, |                                 const submissionOptions = { | ||||||
|                                         false, true, true, siteId).then((subm) => { |                                     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); |                                     return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); | ||||||
|                                 })); |                                 })); | ||||||
|                             } |                             } | ||||||
| @ -353,7 +381,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | |||||||
|                             return Promise.all(subPromises); |                             return Promise.all(subPromises); | ||||||
|                         }).then(() => { |                         }).then(() => { | ||||||
|                             // Participiants already fetched, we don't need to ignore cache now.
 |                             // 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); |                                 return this.userProvider.prefetchUserAvatars(participants, 'profileimageurl', siteId); | ||||||
|                             }).catch(() => { |                             }).catch(() => { | ||||||
|                                 // Fail silently (Moodle < 3.2).
 |                                 // 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.
 |             // Prefetch own submission, we need to do this for teachers too so the response with error is cached.
 | ||||||
|             promises.push( |             promises.push( | ||||||
|                 this.assignProvider.getSubmissionStatusWithRetry(assign, userId, undefined, false, true, true, siteId) |                 this.assignProvider.getSubmissionStatusWithRetry(assign, { | ||||||
|                         .then((subm) => { |                     userId, | ||||||
|  |                     readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||||
|  |                     siteId, | ||||||
|  |                 }).then((subm) => { | ||||||
|                     return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); |                     return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); | ||||||
|                 }).catch((error) => { |                 }).catch((error) => { | ||||||
|                     // Ignore if the user can't view their own submission.
 |                     // 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 *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 [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="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-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; | |||||||
| import { CoreFileProvider } from '@providers/file'; | import { CoreFileProvider } from '@providers/file'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| @ -73,11 +73,11 @@ export class AddonModBookProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param cmId Course module 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. |      * @return Promise resolved when the book is retrieved. | ||||||
|      */ |      */ | ||||||
|     getBook(courseId: number, cmId: number, siteId?: string): Promise<AddonModBookBook> { |     getBook(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModBookBook> { | ||||||
|         return this.getBookByField(courseId, 'coursemodule', cmId, siteId); |         return this.getBookByField(courseId, 'coursemodule', cmId, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -89,14 +89,18 @@ export class AddonModBookProvider { | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved when the book is retrieved. |      * @return Promise resolved when the book is retrieved. | ||||||
|      */ |      */ | ||||||
|     protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise<AddonModBookBook> { |     protected getBookByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |             : Promise<AddonModBookBook> { | ||||||
|  | 
 | ||||||
|  |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 courseids: [courseId] |                 courseids: [courseId] | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getBookDataCacheKey(courseId), |                 cacheKey: this.getBookDataCacheKey(courseId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_RARELY |                 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) |             return site.read('mod_book_get_books_by_courses', params, preSets) | ||||||
|  | |||||||
| @ -127,7 +127,8 @@ export class AddonModChatChatPage { | |||||||
|     showChatUsers(): void { |     showChatUsers(): void { | ||||||
|         // Create the toc modal.
 |         // Create the toc modal.
 | ||||||
|         const modal =  this.modalCtrl.create('AddonModChatUsersPage', { |         const modal =  this.modalCtrl.create('AddonModChatUsersPage', { | ||||||
|             sessionId: this.sessionId |             sessionId: this.sessionId, | ||||||
|  |             cmId: this.cmId, | ||||||
|         }, { cssClass: 'core-modal-lateral', |         }, { cssClass: 'core-modal-lateral', | ||||||
|             showBackdrop: true, |             showBackdrop: true, | ||||||
|             enableBackdropDismiss: true, |             enableBackdropDismiss: true, | ||||||
| @ -168,7 +169,7 @@ export class AddonModChatChatPage { | |||||||
|             return Promise.resolve(user.fullname); |             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; |             this.users = data.users; | ||||||
|             const user = this.users.find((user) => user.id == id); |             const user = this.users.find((user) => user.id == id); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -60,8 +60,8 @@ export class AddonModChatSessionMessagesPage { | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected fetchMessages(): Promise<any> { |     protected fetchMessages(): Promise<any> { | ||||||
|         return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId) |         return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId, | ||||||
|                 .then((messages) => { |                 {cmId: this.cmId}).then((messages) => { | ||||||
|             return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => { |             return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => { | ||||||
|                 this.messages = <AddonModChatSessionMessageForView[]> messages; |                 this.messages = <AddonModChatSessionMessageForView[]> messages; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -72,7 +72,7 @@ export class AddonModChatSessionsPage { | |||||||
|             this.groupInfo = groupInfo; |             this.groupInfo = groupInfo; | ||||||
|             this.groupId = this.groupsProvider.validateGroupId(this.groupId, 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[]) => { |         }).then((sessions: AddonModChatSessionFormatted[]) => { | ||||||
|             // Fetch user profiles.
 |             // Fetch user profiles.
 | ||||||
|             const promises = []; |             const promises = []; | ||||||
|  | |||||||
| @ -36,6 +36,7 @@ export class AddonModChatUsersPage { | |||||||
|     isOnline: boolean; |     isOnline: boolean; | ||||||
| 
 | 
 | ||||||
|     protected sessionId: string; |     protected sessionId: string; | ||||||
|  |     protected cmId: number; | ||||||
|     protected onlineObserver: any; |     protected onlineObserver: any; | ||||||
| 
 | 
 | ||||||
|     constructor(navParams: NavParams, network: Network,  zone: NgZone, private appProvider: CoreAppProvider, |     constructor(navParams: NavParams, network: Network,  zone: NgZone, private appProvider: CoreAppProvider, | ||||||
| @ -56,7 +57,7 @@ export class AddonModChatUsersPage { | |||||||
|      * View loaded. |      * View loaded. | ||||||
|      */ |      */ | ||||||
|     ionViewDidLoad(): void { |     ionViewDidLoad(): void { | ||||||
|         this.chatProvider.getChatUsers(this.sessionId).then((data) => { |         this.chatProvider.getChatUsers(this.sessionId, {cmId: this.cmId}).then((data) => { | ||||||
|             this.users = data.users; |             this.users = data.users; | ||||||
|         }).catch((error) => { |         }).catch((error) => { | ||||||
|             this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhilegettingchatusers', true); |             this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhilegettingchatusers', true); | ||||||
|  | |||||||
| @ -14,13 +14,14 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { TranslateService } from '@ngx-translate/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 { CoreUserProvider } from '@core/user/providers/user'; | ||||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | import { CoreSite } from '@classes/site'; | ||||||
| import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; | import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; | ||||||
| import { AddonModChatMessageForView, AddonModChatSessionMessageForView } from './helper'; | import { AddonModChatMessageForView, AddonModChatSessionMessageForView } from './helper'; | ||||||
|  | import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service that provides some features for chats. |  * Service that provides some features for chats. | ||||||
| @ -40,17 +41,19 @@ export class AddonModChatProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param cmId Course module 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. |      * @return Promise resolved when the chat is retrieved. | ||||||
|      */ |      */ | ||||||
|     getChat(courseId: number, cmId: number, siteId?: string): Promise<AddonModChatChat> { |     getChat(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChatChat> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 courseids: [courseId] |                 courseids: [courseId] | ||||||
|             }; |             }; | ||||||
|             const preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getChatsCacheKey(courseId), |                 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) |             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. |      * Get the actives users of a current chat. | ||||||
|      * |      * | ||||||
|      * @param sessionId Chat sessiond ID. |      * @param sessionId Chat sessiond ID. | ||||||
|  |      * @param options Other options. | ||||||
|      * @return Promise resolved when the WS is executed. |      * @return Promise resolved when the WS is executed. | ||||||
|      */ |      */ | ||||||
|     getChatUsers(sessionId: string): Promise<AddonModChatGetChatUsersResult> { |     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.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|             chatsid: sessionId |                 chatsid: sessionId, | ||||||
|             }; |             }; | ||||||
|             const preSets = { |             const preSets = { | ||||||
|             getFromCache: false |                 component: AddonModChatProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets); |             return site.read('mod_chat_get_chat_users', params, preSets); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -210,28 +221,26 @@ export class AddonModChatProvider { | |||||||
|      * @param chatId Chat ID. |      * @param chatId Chat ID. | ||||||
|      * @param groupId Group ID, 0 means that the function will determine the user group. |      * @param groupId Group ID, 0 means that the function will determine the user group. | ||||||
|      * @param showAll Whether to include incomplete sessions or not. |      * @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 options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with the list of sessions. |      * @return Promise resolved with the list of sessions. | ||||||
|      * @since 3.5 |      * @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[]> { |             Promise<AddonModChatSession[]> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 chatid: chatId, |                 chatid: chatId, | ||||||
|                 groupid: groupId, |                 groupid: groupId, | ||||||
|                 showall: showAll ? 1 : 0 |                 showall: showAll ? 1 : 0, | ||||||
|             }; |             }; | ||||||
|             const preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getSessionsCacheKey(chatId, groupId, showAll), |                 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 => { |             return site.read('mod_chat_get_sessions', params, preSets).then((response: AddonModChatGetSessionsResult): any => { | ||||||
|                 if (!response || !response.sessions) { |                 if (!response || !response.sessions) { | ||||||
| @ -250,29 +259,27 @@ export class AddonModChatProvider { | |||||||
|      * @param sessionStart Session start time. |      * @param sessionStart Session start time. | ||||||
|      * @param sessionEnd Session end time. |      * @param sessionEnd Session end time. | ||||||
|      * @param groupId Group ID, 0 means that the function will determine the user group. |      * @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 options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with the list of messages. |      * @return Promise resolved with the list of messages. | ||||||
|      * @since 3.5 |      * @since 3.5 | ||||||
|      */ |      */ | ||||||
|     getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, ignoreCache: boolean = false, |     getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, | ||||||
|             siteId?: string): Promise<AddonModChatSessionMessage[]> { |             options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChatSessionMessage[]> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 chatid: chatId, |                 chatid: chatId, | ||||||
|                 sessionstart: sessionStart, |                 sessionstart: sessionStart, | ||||||
|                 sessionend: sessionEnd, |                 sessionend: sessionEnd, | ||||||
|                 groupid: groupId |                 groupid: groupId, | ||||||
|             }; |             }; | ||||||
|             const preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getSessionMessagesCacheKey(chatId, sessionStart, groupId), |                 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) |             return site.read('mod_chat_get_session_messages', params, preSets) | ||||||
|                     .then((response: AddonModChatGetSessionMessagesResult): any => { |                     .then((response: AddonModChatGetSessionMessagesResult): any => { | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; | |||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | 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> { |     protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { | ||||||
|         // Prefetch chat and group info.
 |         // Prefetch chat and group info.
 | ||||||
|         const promises: Promise<any>[] = [ |         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) |             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]) => { |         return Promise.all(promises).then(([chat, groupInfo]: [AddonModChatChat, CoreGroupInfo]) => { | ||||||
|             const promises = []; |             const promises = []; | ||||||
| @ -136,7 +141,7 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
| 
 | 
 | ||||||
|             groupIds.forEach((groupId) => { |             groupIds.forEach((groupId) => { | ||||||
|                 // Prefetch complete sessions.
 |                 // 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.
 |                     // Ignore group error.
 | ||||||
|                     if (error.errorcode != 'notingroup') { |                     if (error.errorcode != 'notingroup') { | ||||||
|                         return Promise.reject(error); |                         return Promise.reject(error); | ||||||
| @ -144,8 +149,9 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
|                 })); |                 })); | ||||||
| 
 | 
 | ||||||
|                 // Prefetch all sessions.
 |                 // Prefetch all sessions.
 | ||||||
|                 promises.push(this.chatProvider.getSessions(chat.id, groupId, true, true, siteId).then((sessions) => { |                 promises.push(this.chatProvider.getSessions(chat.id, groupId, true, options).then((sessions) => { | ||||||
|                     const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, siteId)); |                     const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, module.id, | ||||||
|  |                             siteId)); | ||||||
| 
 | 
 | ||||||
|                     return Promise.all(promises); |                     return Promise.all(promises); | ||||||
|                 }).catch((error) => { |                 }).catch((error) => { | ||||||
| @ -170,9 +176,13 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
|      * @param siteId Site ID. |      * @param siteId Site ID. | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise<any> { |     protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, cmId: number, siteId: string) | ||||||
|         return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId) |             : Promise<any> { | ||||||
|                 .then((messages) => { |         return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, { | ||||||
|  |             cmId, | ||||||
|  |             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||||
|  |             siteId, | ||||||
|  |         }).then((messages) => { | ||||||
|             const users = {}; |             const users = {}; | ||||||
|             session.sessionusers.forEach((user) => { |             session.sessionusers.forEach((user) => { | ||||||
|                 users[user.userid] = true; |                 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]="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="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="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-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
| @ -43,13 +43,17 @@ | |||||||
|     <ion-card *ngIf="options && options.length"> |     <ion-card *ngIf="options && options.length"> | ||||||
|         <ng-container *ngIf="choice.allowmultiple"> |         <ng-container *ngIf="choice.allowmultiple"> | ||||||
|             <ion-item text-wrap *ngFor="let option of options"> |             <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-checkbox item-end [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit"></ion-checkbox> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|         <ng-container *ngIf="!choice.allowmultiple"> |         <ng-container *ngIf="!choice.allowmultiple"> | ||||||
|             <ion-item text-wrap *ngFor="let option of options" radio-group [(ngModel)]="selectedOption.id"> |             <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-radio color="primary" [value]="option.id" [disabled]="option.disabled || !canEdit"></ion-radio> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|         </ng-container> |         </ng-container> | ||||||
| @ -81,6 +85,7 @@ | |||||||
|                         <ion-item-divider text-wrap> |                         <ion-item-divider text-wrap> | ||||||
|                             <h2><core-format-text [text]="result.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text></h2> |                             <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>{{ '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> |                         </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> |                         <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> |                             <ion-avatar core-user-avatar [user]="user" item-start [courseId]="courseid"></ion-avatar> | ||||||
| @ -95,3 +100,14 @@ | |||||||
|         <p>{{ 'addon.mod_choice.noresultsviewable' | translate }}</p> |         <p>{{ 'addon.mod_choice.noresultsviewable' | translate }}</p> | ||||||
|     </ion-card> |     </ion-card> | ||||||
| </core-loading> | </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. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected fetchOptions(hasOffline: boolean): Promise<any> { |     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; |             let promise; | ||||||
| 
 | 
 | ||||||
|             // Check if the user has answered (synced) to allow show results.
 |             // Check if the user has answered (synced) to allow show results.
 | ||||||
| @ -294,7 +294,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|             return Promise.resolve(); |             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; |             let hasVotes = false; | ||||||
|             this.data = []; |             this.data = []; | ||||||
|             this.labels = []; |             this.labels = []; | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ | |||||||
|     "errorgetchoice": "Error getting choice data.", |     "errorgetchoice": "Error getting choice data.", | ||||||
|     "expired": "This activity closed on {{$a}}.", |     "expired": "This activity closed on {{$a}}.", | ||||||
|     "full": "(Full)", |     "full": "(Full)", | ||||||
|  |     "limita": "Limit: {{$a}}", | ||||||
|     "modulenameplural": "Choices", |     "modulenameplural": "Choices", | ||||||
|     "noresultsviewable": "The results are not currently viewable.", |     "noresultsviewable": "The results are not currently viewable.", | ||||||
|     "notopenyet": "This activity is not available until {{$a}}.", |     "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.", |     "publishinfonever": "The results of this activity will not be published after you answer.", | ||||||
|     "removemychoice": "Remove my choice", |     "removemychoice": "Remove my choice", | ||||||
|     "responses": "Responses", |     "responses": "Responses", | ||||||
|  |     "responsesa": "Responses: {{$a}}", | ||||||
|     "responsesresultgraphdescription": "{{number}}% of the users chose the option: {{text}}.", |     "responsesresultgraphdescription": "{{number}}% of the users chose the option: {{text}}.", | ||||||
|     "responsesresultgraphheader": "Graph display", |     "responsesresultgraphheader": "Graph display", | ||||||
|     "resultsnotsynced": "Your last response must be synchronised before it is included in the results.", |     "resultsnotsynced": "Your last response must be synchronised before it is included in the results.", | ||||||
|  | |||||||
| @ -13,14 +13,15 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| import { AddonModChoiceOfflineProvider } from './offline'; | import { AddonModChoiceOfflineProvider } from './offline'; | ||||||
| import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | import { CoreSite } from '@classes/site'; | ||||||
| import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; | import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; | ||||||
|  | import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service that provides some features for choices. |  * 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. |      * 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 courseId Course ID. | ||||||
|      * @param key Name of the property to check. |      * @param key Name of the property to check. | ||||||
|      * @param value Value to search. |      * @param value Value to search. | ||||||
|      * @param forceCache True to always get the value from cache, false otherwise. Default false. |      * @param options Other options. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |  | ||||||
|      * @return Promise resolved when the choice is retrieved. |      * @return Promise resolved when the choice is retrieved. | ||||||
|      */ |      */ | ||||||
|     protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache?: boolean, |     protected getChoiceByDataKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) | ||||||
|             ignoreCache?: boolean): Promise<AddonModChoiceChoice> { |             : Promise<AddonModChoiceChoice> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 courseids: [courseId] |                 courseids: [courseId] | ||||||
|             }; |             }; | ||||||
|             const preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getChoiceDataCacheKey(courseId), |                 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) |             return site.read('mod_choice_get_choices_by_courses', params, preSets) | ||||||
|                     .then((response: AddonModChoiceGetChoicesByCoursesResult): any => { |                     .then((response: AddonModChoiceGetChoicesByCoursesResult): any => { | ||||||
| 
 | 
 | ||||||
| @ -221,14 +214,11 @@ export class AddonModChoiceProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param cmId Course module ID. |      * @param cmId Course module ID. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param options Other options. | ||||||
|      * @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). |  | ||||||
|      * @return Promise resolved when the choice is retrieved. |      * @return Promise resolved when the choice is retrieved. | ||||||
|      */ |      */ | ||||||
|     getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean) |     getChoice(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChoiceChoice> { | ||||||
|             : Promise<AddonModChoiceChoice> { |         return this.getChoiceByDataKey(courseId, 'coursemodule', cmId, options); | ||||||
|         return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -236,39 +226,33 @@ export class AddonModChoiceProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param choiceId Choice ID. |      * @param choiceId Choice ID. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param options Other options. | ||||||
|      * @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). |  | ||||||
|      * @return Promise resolved when the choice is retrieved. |      * @return Promise resolved when the choice is retrieved. | ||||||
|      */ |      */ | ||||||
|     getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean) |     getChoiceById(courseId: number, choiceId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChoiceChoice> { | ||||||
|             : Promise<AddonModChoiceChoice> { |         return this.getChoiceByDataKey(courseId, 'id', choiceId, options); | ||||||
|         return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get choice options. |      * Get choice options. | ||||||
|      * |      * | ||||||
|      * @param choiceId Choice ID. |      * @param choiceId Choice ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with choice options. |      * @return Promise resolved with choice options. | ||||||
|      */ |      */ | ||||||
|     getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModChoiceOption[]> { |     getOptions(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChoiceOption[]> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 choiceid: choiceId |                 choiceid: choiceId | ||||||
|             }; |             }; | ||||||
|             const preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getChoiceOptionsCacheKey(choiceId), |                 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) |             return site.read('mod_choice_get_choice_options', params, preSets) | ||||||
|                     .then((response: AddonModChoiceGetChoiceOptionsResult): any => { |                     .then((response: AddonModChoiceGetChoiceOptionsResult): any => { | ||||||
| 
 | 
 | ||||||
| @ -285,24 +269,21 @@ export class AddonModChoiceProvider { | |||||||
|      * Get choice results. |      * Get choice results. | ||||||
|      * |      * | ||||||
|      * @param choiceId Choice ID. |      * @param choiceId Choice ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with choice results. |      * @return Promise resolved with choice results. | ||||||
|      */ |      */ | ||||||
|     getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModChoiceResult[]> { |     getResults(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChoiceResult[]> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 choiceid: choiceId |                 choiceid: choiceId | ||||||
|             }; |             }; | ||||||
|             const preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getChoiceResultsCacheKey(choiceId) |                 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) |             return site.read('mod_choice_get_choice_results', params, preSets) | ||||||
|                     .then((response: AddonModChoiceGetChoiceResults): any => { |                     .then((response: AddonModChoiceGetChoiceResults): any => { | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; | |||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | import { CoreCourseProvider } from '@core/course/providers/course'; | ||||||
| @ -79,12 +79,21 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected prefetchChoice(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { |     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 = []; |             const promises = []; | ||||||
| 
 | 
 | ||||||
|             // Get the options and results.
 |             // Get the options and results.
 | ||||||
|             promises.push(this.choiceProvider.getOptions(choice.id, true, siteId)); |             promises.push(this.choiceProvider.getOptions(choice.id, modOptions)); | ||||||
|             promises.push(this.choiceProvider.getResults(choice.id, true, siteId).then((options) => { |             promises.push(this.choiceProvider.getResults(choice.id, modOptions).then((options) => { | ||||||
|                 // If we can see the users that answered, prefetch their profile and avatar.
 |                 // If we can see the users that answered, prefetch their profile and avatar.
 | ||||||
|                 const subPromises = []; |                 const subPromises = []; | ||||||
|                 options.forEach((option) => { |                 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]="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 [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="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-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -180,29 +180,34 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|      * @param showErrors If show errors to the user of hide them. |      * @param showErrors If show errors to the user of hide them. | ||||||
|      * @return Promise resolved when done. |      * @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, |         let canAdd = false, | ||||||
|             canSearch = false; |             canSearch = false; | ||||||
| 
 | 
 | ||||||
|         return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { |         this.data = await this.dataProvider.getDatabase(this.courseId, this.module.id); | ||||||
|             this.data = data; |         this.hasComments = this.data.comments; | ||||||
|             this.hasComments = data.comments; |  | ||||||
| 
 | 
 | ||||||
|             this.description = data.intro || data.description; |         this.description = this.data.intro || this.data.description; | ||||||
|             this.dataRetrieved.emit(data); |         this.dataRetrieved.emit(this.data); | ||||||
| 
 | 
 | ||||||
|         if (sync) { |         if (sync) { | ||||||
|  |             try { | ||||||
|                 // Try to synchronize the data.
 |                 // Try to synchronize the data.
 | ||||||
|                 return this.syncActivity(showErrors).catch(() => { |                 await this.syncActivity(showErrors); | ||||||
|  |             } catch (error) { | ||||||
|                 // Ignore errors.
 |                 // Ignore errors.
 | ||||||
|                 }); |  | ||||||
|             } |             } | ||||||
|         }).then(() => { |         } | ||||||
|             return this.dataProvider.getDatabaseAccessInformation(this.data.id); |  | ||||||
|         }).then((accessData) => { |  | ||||||
|             this.access = accessData; |  | ||||||
| 
 | 
 | ||||||
|             if (!accessData.timeavailable) { |         this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.data.coursemodule); | ||||||
|  |         this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, this.groupInfo); | ||||||
|  | 
 | ||||||
|  |         this.access = await this.dataProvider.getDatabaseAccessInformation(this.data.id, { | ||||||
|  |             cmId: this.module.id, | ||||||
|  |             groupId: this.selectedGroup || undefined | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         if (!this.access.timeavailable) { | ||||||
|             const time = this.timeUtils.timestamp(); |             const time = this.timeUtils.timestamp(); | ||||||
| 
 | 
 | ||||||
|             this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ? |             this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ? | ||||||
| @ -214,35 +219,28 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
| 
 | 
 | ||||||
|             this.isEmpty = true; |             this.isEmpty = true; | ||||||
|             this.groupInfo = null; |             this.groupInfo = null; | ||||||
| 
 |         } else { | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             canSearch = true; |             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; |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         const fields = await this.dataProvider.getFields(this.data.id, {cmId: this.module.id}); | ||||||
|         this.search.advanced = []; |         this.search.advanced = []; | ||||||
| 
 | 
 | ||||||
|         this.fields = this.utils.arrayToObject(fields, 'id'); |         this.fields = this.utils.arrayToObject(fields, 'id'); | ||||||
|         this.fieldsArray = this.utils.objectToArray(this.fields); |         this.fieldsArray = this.utils.objectToArray(this.fields); | ||||||
|  |         if (this.fieldsArray.length == 0) { | ||||||
|  |             canSearch = false; | ||||||
|  |             canAdd = false; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|                 return this.fetchEntriesData(); |         try { | ||||||
|             }); |             await this.fetchEntriesData(); | ||||||
|         }).finally(() => { |         } finally { | ||||||
|             this.canAdd = canAdd; |             this.canAdd = canAdd; | ||||||
|             this.canSearch = canSearch; |             this.canSearch = canSearch; | ||||||
|             this.fillContextMenu(refresh); |             this.fillContextMenu(refresh); | ||||||
|         }); |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -252,15 +250,17 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|      */ |      */ | ||||||
|     protected fetchEntriesData(): Promise<any> { |     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 search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined; | ||||||
|         const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : 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, |         return this.dataHelper.fetchEntries(this.data, this.fieldsArray, { | ||||||
|                     this.search.sortBy, this.search.sortDirection, this.search.page); |             groupId: this.selectedGroup, | ||||||
|  |             search, | ||||||
|  |             advSearch, | ||||||
|  |             sort: Number(this.search.sortBy), | ||||||
|  |             order: this.search.sortDirection, | ||||||
|  |             page: this.search.page, | ||||||
|  |             cmId: this.module.id, | ||||||
|         }).then((entries) => { |         }).then((entries) => { | ||||||
|             const numEntries = entries.entries.length; |             const numEntries = entries.entries.length; | ||||||
|             const numOfflineEntries = entries.offlineEntries.length; |             const numOfflineEntries = entries.offlineEntries.length; | ||||||
| @ -381,18 +381,29 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|      * @param groupId Group ID. |      * @param groupId Group ID. | ||||||
|      * @return Resolved when new group is selected or rejected if not. |      * @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.selectedGroup = groupId; | ||||||
|         this.search.page = 0; |         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.
 |             // Log activity view for coherence with Moodle web.
 | ||||||
|             return this.logView(); |             return this.logView(); | ||||||
|         }).catch((message) => { |         } catch (error) { | ||||||
|             this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); |             this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); | ||||||
| 
 |         } | ||||||
|             return Promise.reject(null); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -14,10 +14,9 @@ | |||||||
| import { Component } from '@angular/core'; | import { Component } from '@angular/core'; | ||||||
| import { FormBuilder } from '@angular/forms'; | import { FormBuilder } from '@angular/forms'; | ||||||
| import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; | 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 { 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'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -33,15 +32,14 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo | |||||||
|     east: number; |     east: number; | ||||||
|     showGeolocation: boolean; |     showGeolocation: boolean; | ||||||
| 
 | 
 | ||||||
|     constructor(protected fb: FormBuilder, |     constructor( | ||||||
|             protected platform: Platform, |             protected fb: FormBuilder, | ||||||
|             protected geolocation: Geolocation, |  | ||||||
|             protected domUtils: CoreDomUtilsProvider, |             protected domUtils: CoreDomUtilsProvider, | ||||||
|             protected sanitizer: DomSanitizer, |             protected sanitizer: DomSanitizer, | ||||||
|             protected appProvider: CoreAppProvider) { |             appProvider: CoreAppProvider) { | ||||||
|         super(fb); |         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'; |             const eastFixed = east ? east.toFixed(4) : '0.0000'; | ||||||
|             let url; |             let url; | ||||||
| 
 | 
 | ||||||
|             if (this.platform.is('ios')) { |             if (CoreApp.instance.isIOS()) { | ||||||
|                 url = 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed; |                 url = 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed; | ||||||
|             } else { |             } else { | ||||||
|                 url = 'geo:' + northFixed + ',' + eastFixed; |                 url = 'geo:' + northFixed + ',' + eastFixed; | ||||||
| @ -115,33 +113,51 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo | |||||||
|      * |      * | ||||||
|      * @param $event The event. |      * @param $event The event. | ||||||
|      */ |      */ | ||||||
|     getLocation(event: Event): void { |     async getLocation(event: Event): Promise<void> { | ||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
| 
 | 
 | ||||||
|         const modal = this.domUtils.showModalLoading('addon.mod_data.gettinglocation', true); |         const modal = this.domUtils.showModalLoading('addon.mod_data.gettinglocation', true); | ||||||
| 
 | 
 | ||||||
|         const options: GeolocationOptions = { |         try { | ||||||
|             enableHighAccuracy: true, |             const coordinates = await CoreGeolocation.instance.getCoordinates(); | ||||||
|             timeout: 30000 |  | ||||||
|         }; |  | ||||||
| 
 | 
 | ||||||
|         this.geolocation.getCurrentPosition(options).then((result) => { |             this.form.controls['f_' + this.field.id + '_0'].setValue(coordinates.latitude); | ||||||
|             this.form.controls['f_' + this.field.id + '_0'].setValue(result.coords.latitude); |             this.form.controls['f_' + this.field.id + '_1'].setValue(coordinates.longitude); | ||||||
|             this.form.controls['f_' + this.field.id + '_1'].setValue(result.coords.longitude); |         } catch (error) { | ||||||
|         }).catch((error) => { |             this.showLocationErrorModal(error); | ||||||
|             if (this.isPermissionDeniedError(error)) { |         } | ||||||
|                 this.domUtils.showErrorModal('addon.mod_data.locationpermissiondenied', true); | 
 | ||||||
|  |         modal.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.domUtils.showErrorModalDefault(error,  'Error getting location'); |         this.domUtils.showErrorModalDefault(error,  'Error getting location'); | ||||||
|         }).finally(() => { |  | ||||||
|             modal.dismiss(); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected isPermissionDeniedError(error?: any): boolean { |     /** | ||||||
|         return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; |      * 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", |     "gettinglocation": "Getting location", | ||||||
|     "latlongboth": "Both latitude and longitude are required.", |     "latlongboth": "Both latitude and longitude are required.", | ||||||
|     "locationpermissiondenied": "Permission to access your location has been denied.", |     "locationpermissiondenied": "Permission to access your location has been denied.", | ||||||
|  |     "locationnotenabled": "Location is not enabled", | ||||||
|     "menuchoose": "Choose...", |     "menuchoose": "Choose...", | ||||||
|     "modulenameplural": "Databases", |     "modulenameplural": "Databases", | ||||||
|     "more": "More", |     "more": "More", | ||||||
|     "mylocation": "My location", |     "mylocation": "My location", | ||||||
|  |     "noaccess": "You do not have access to this page", | ||||||
|     "nomatch": "No matching entries found!", |     "nomatch": "No matching entries found!", | ||||||
|     "norecords": "No entries in database", |     "norecords": "No entries in database", | ||||||
|     "notapproved": "Entry is not approved yet.", |     "notapproved": "Entry is not approved yet.", | ||||||
|  | |||||||
| @ -18,8 +18,8 @@ | |||||||
|             </ion-select> |             </ion-select> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| 
 | 
 | ||||||
|         <div class="addon-data-contents addon-data-entries-{{data.id}}" *ngIf="data"> |         <div class="addon-data-contents {{cssClass}}" *ngIf="data"> | ||||||
|             <core-style [css]="data.csstemplate" prefix=".addon-data-entries-{{data.id}}"></core-style> |             <core-style [css]="data.csstemplate" prefix=".{{cssClass}}"></core-style> | ||||||
| 
 | 
 | ||||||
|             <form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl> |             <form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl> | ||||||
|                 <core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html> |                 <core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html> | ||||||
|  | |||||||
| @ -52,6 +52,8 @@ export class AddonModDataEditPage { | |||||||
|     protected siteId: string; |     protected siteId: string; | ||||||
|     protected offline: boolean; |     protected offline: boolean; | ||||||
|     protected forceLeave = false; // To allow leaving the page without checking for changes.
 |     protected forceLeave = false; // To allow leaving the page without checking for changes.
 | ||||||
|  |     protected initialSelectedGroup = null; | ||||||
|  |     protected isEditing = false; | ||||||
| 
 | 
 | ||||||
|     title = ''; |     title = ''; | ||||||
|     component = AddonModDataProvider.COMPONENT; |     component = AddonModDataProvider.COMPONENT; | ||||||
| @ -75,7 +77,10 @@ export class AddonModDataEditPage { | |||||||
|         this.module = params.get('module') || {}; |         this.module = params.get('module') || {}; | ||||||
|         this.entryId = params.get('entryId') || null; |         this.entryId = params.get('entryId') || null; | ||||||
|         this.courseId = params.get('courseId'); |         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(); |         this.siteId = sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
| @ -88,7 +93,7 @@ export class AddonModDataEditPage { | |||||||
|      * View loaded. |      * View loaded. | ||||||
|      */ |      */ | ||||||
|     ionViewDidLoad(): void { |     ionViewDidLoad(): void { | ||||||
|         this.fetchEntryData(); |         this.fetchEntryData(true); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -103,7 +108,8 @@ export class AddonModDataEditPage { | |||||||
| 
 | 
 | ||||||
|         const inputData = this.editForm.value; |         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) { |         if (changed) { | ||||||
|             // Show confirmation if some data has been modified.
 |             // Show confirmation if some data has been modified.
 | ||||||
| @ -120,38 +126,78 @@ export class AddonModDataEditPage { | |||||||
|     /** |     /** | ||||||
|      * Fetch the entry data. |      * Fetch the entry data. | ||||||
|      * |      * | ||||||
|  |      * @param [refresh] To refresh all downloaded data. | ||||||
|      * @return Resolved when done. |      * @return Resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected fetchEntryData(): Promise<any> { |     protected async fetchEntryData(refresh: boolean = false): Promise<void> { | ||||||
|         return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { |         try { | ||||||
|             this.title = data.name || this.title; |             this.data = await this.dataProvider.getDatabase(this.courseId, this.module.id); | ||||||
|             this.data = data; |             this.title = this.data.name || this.title; | ||||||
|             this.cssClass = 'addon-data-entries-' + data.id; |             this.cssClass = 'addon-data-entries-' + this.data.id; | ||||||
| 
 | 
 | ||||||
|             return this.dataProvider.getDatabaseAccessInformation(data.id); |             this.fieldsArray = await this.dataProvider.getFields(this.data.id, {cmId: this.module.id}); | ||||||
|         }).then((accessData) => { |             this.fields = this.utils.arrayToObject(this.fieldsArray, 'id'); | ||||||
|             if (this.entryId) { | 
 | ||||||
|                 return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => { |             const entry = await this.dataHelper.fetchEntry(this.data, this.fieldsArray, this.entryId); | ||||||
|                     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'); |  | ||||||
| 
 | 
 | ||||||
|             return this.dataHelper.fetchEntry(this.data, fieldsData, this.entryId); |  | ||||||
|         }).then((entry) => { |  | ||||||
|             this.entry = entry.entry; |             this.entry = entry.entry; | ||||||
| 
 | 
 | ||||||
|             this.editFormRender = this.displayEditFields(); |             // Load correct group.
 | ||||||
|         }).catch((message) => { |             this.selectedGroup = this.selectedGroup == null ? this.entry.groupid : this.selectedGroup; | ||||||
|             this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); | 
 | ||||||
|         }).finally(() => { |             // Check permissions when adding a new entry or offline entry.
 | ||||||
|             this.loaded = true; |             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) { | ||||||
|  |             this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.loaded = true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -160,7 +206,7 @@ export class AddonModDataEditPage { | |||||||
|      * @param e Event. |      * @param e Event. | ||||||
|      * @return Resolved when done. |      * @return Resolved when done. | ||||||
|      */ |      */ | ||||||
|     save(e: Event): Promise<any> { |     save(e: Event): Promise<void> { | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|         e.stopPropagation(); |         e.stopPropagation(); | ||||||
| 
 | 
 | ||||||
| @ -169,6 +215,7 @@ export class AddonModDataEditPage { | |||||||
|         return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, |         return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, | ||||||
|                 this.entry.contents).then((changed) => { |                 this.entry.contents).then((changed) => { | ||||||
| 
 | 
 | ||||||
|  |             changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup); | ||||||
|             if (!changed) { |             if (!changed) { | ||||||
|                 if (this.entryId) { |                 if (this.entryId) { | ||||||
|                     return this.returnToEntryList(); |                     return this.returnToEntryList(); | ||||||
| @ -196,7 +243,7 @@ export class AddonModDataEditPage { | |||||||
|                     return Promise.reject(e); |                     return Promise.reject(e); | ||||||
|             }).then((editData) => { |             }).then((editData) => { | ||||||
|                 if (editData.length > 0) { |                 if (editData.length > 0) { | ||||||
|                     if (this.entryId) { |                     if (this.isEditing) { | ||||||
|                         return this.dataProvider.editEntry(this.data.id, this.entryId, this.courseId, editData, this.fields, |                         return this.dataProvider.editEntry(this.data.id, this.entryId, this.courseId, editData, this.fields, | ||||||
|                             undefined, this.offline); |                             undefined, this.offline); | ||||||
|                     } |                     } | ||||||
| @ -213,20 +260,20 @@ export class AddonModDataEditPage { | |||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // This is done if entry is updated when editing or creating if not.
 |                 // 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); |                     this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.siteId); | ||||||
| 
 | 
 | ||||||
|                     if (result.sent) { |  | ||||||
|                         this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' }); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     const promises = []; |                     const promises = []; | ||||||
| 
 | 
 | ||||||
|                     this.entryId = this.entryId || result.newentryid; |                     if (result.sent) { | ||||||
|  |                         this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' }); | ||||||
| 
 | 
 | ||||||
|  |                         if (this.isEditing) { | ||||||
|                             promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId)); |                             promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId)); | ||||||
|  |                         } | ||||||
|                         promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId)); |                         promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId)); | ||||||
|  |                     } | ||||||
| 
 | 
 | ||||||
|                     return Promise.all(promises).then(() => { |                     return Promise.all(promises).then(() => { | ||||||
|                         this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, |                         this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, | ||||||
| @ -264,7 +311,7 @@ export class AddonModDataEditPage { | |||||||
|      * @param groupId Group identifier to set. |      * @param groupId Group identifier to set. | ||||||
|      * @return Resolved when done. |      * @return Resolved when done. | ||||||
|      */ |      */ | ||||||
|     setGroup(groupId: number): Promise<any> { |     setGroup(groupId: number): Promise<void> { | ||||||
|         this.selectedGroup = groupId; |         this.selectedGroup = groupId; | ||||||
|         this.loaded = false; |         this.loaded = false; | ||||||
| 
 | 
 | ||||||
| @ -322,7 +369,7 @@ export class AddonModDataEditPage { | |||||||
|      * |      * | ||||||
|      * @return Resolved when done. |      * @return Resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected returnToEntryList(): Promise<any> { |     protected returnToEntryList(): Promise<void> { | ||||||
|         const inputData = this.editForm.value; |         const inputData = this.editForm.value; | ||||||
| 
 | 
 | ||||||
|         return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, |         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.title = data.name || this.title; | ||||||
|             this.data = data; |             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.fields = this.utils.arrayToObject(fieldsData, 'id'); | ||||||
|                 this.fieldsArray = fieldsData; |                 this.fieldsArray = fieldsData; | ||||||
|             }); |             }); | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             return this.setEntryFromOffset().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) => { |         }).then((accessData) => { | ||||||
|             this.access = accessData; |             this.access = accessData; | ||||||
| @ -290,8 +290,13 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|         const perPage = AddonModDataProvider.PER_PAGE; |         const perPage = AddonModDataProvider.PER_PAGE; | ||||||
|         const page = !emptyOffset && this.offset >= 0 ? Math.floor(this.offset / perPage) : 0; |         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', |         return this.dataHelper.fetchEntries(this.data, this.fieldsArray, { | ||||||
|                 page, perPage).then((entries) => { |             groupId: this.selectedGroup, | ||||||
|  |             sort: 0, | ||||||
|  |             order: 'DESC', | ||||||
|  |             page, | ||||||
|  |             perPage, | ||||||
|  |         }).then((entries) => { | ||||||
| 
 | 
 | ||||||
|             const pageEntries = entries.offlineEntries.concat(entries.entries); |             const pageEntries = entries.offlineEntries.concat(entries.entries); | ||||||
|             let pageIndex; // Index of the entry when concatenating offline and online page 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; |                 this.nextOffset = null; | ||||||
|             } else { |             } else { | ||||||
|                 // Last entry of the page, check if there are more pages.
 |                 // 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) |                 promise = this.dataProvider.getEntries(this.data.id, { | ||||||
|                         .then((entries) => { |                     groupId: this.selectedGroup, | ||||||
|  |                     page: page + 1, | ||||||
|  |                     perPage: perPage, | ||||||
|  |                 }).then((entries) => { | ||||||
|                     this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? this.offset + 1 : null; |                     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(() => { |             return Promise.resolve(promise).then(() => { | ||||||
|                 if (this.entryId > 0) { |                 if (this.entryId > 0) { | ||||||
|                     // Online entry, we need to fetch the the rating info.
 |                     // 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; |                         this.ratingInfo = entry.ratinginfo; | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ | |||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| @ -23,6 +23,7 @@ import { AddonModDataOfflineProvider } from './offline'; | |||||||
| import { AddonModDataFieldsDelegate } from './fields-delegate'; | import { AddonModDataFieldsDelegate } from './fields-delegate'; | ||||||
| import { CoreRatingInfo } from '@core/rating/providers/rating'; | import { CoreRatingInfo } from '@core/rating/providers/rating'; | ||||||
| import { CoreSite } from '@classes/site'; | import { CoreSite } from '@classes/site'; | ||||||
|  | import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Database entry (online or offline). |  * Database entry (online or offline). | ||||||
| @ -116,46 +117,51 @@ export class AddonModDataProvider { | |||||||
|      * @param forceOffline Force editing entry in offline. |      * @param forceOffline Force editing entry in offline. | ||||||
|      * @return Promise resolved when the action is done. |      * @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> { |             fields: any, siteId?: string, forceOffline: boolean = false): Promise<any> { | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         // Convenience function to store a data to be synchronized later.
 |         // Convenience function to store a data to be synchronized later.
 | ||||||
|         const storeOffline = (): Promise<any> => { |         const storeOffline = async (): Promise<any> => { | ||||||
|             return this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId) |             const entry = await this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId); | ||||||
|                     .then((entry) => { | 
 | ||||||
|             return { |             return { | ||||||
|                 // Return provissional entry Id.
 |                 // Return provissional entry Id.
 | ||||||
|                 newentryid: entry, |                 newentryid: entry, | ||||||
|                 sent: false, |                 sent: false, | ||||||
|             }; |             }; | ||||||
|             }); |  | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|  |         // Checks to store offline.
 | ||||||
|         if (!this.appProvider.isOnline() || forceOffline) { |         if (!this.appProvider.isOnline() || forceOffline) { | ||||||
|             const notifications = this.checkFields(fields, contents); |             const notifications = this.checkFields(fields, contents); | ||||||
|             if (notifications) { |             if (notifications) { | ||||||
|                 return Promise.resolve({ |                 return { fieldnotifications: notifications }; | ||||||
|                     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 storeOffline(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return this.addEntryOnline(dataId, contents, groupId, siteId).then((result) => { |         try { | ||||||
|  |             const result = await this.addEntryOnline(dataId, contents, groupId, siteId); | ||||||
|             result.sent = true; |             result.sent = true; | ||||||
| 
 | 
 | ||||||
|             return result; |             return result; | ||||||
|         }).catch((error) => { |         } catch (error) { | ||||||
|             if (this.utils.isWebServiceError(error)) { |             if (this.utils.isWebServiceError(error)) { | ||||||
|                 // The WebService has thrown an error, this means that responses cannot be submitted.
 |                 // 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.
 |             // Couldn't connect to server, store in offline.
 | ||||||
|             return storeOffline(); |             return storeOffline(); | ||||||
|         }); |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -192,48 +198,49 @@ export class AddonModDataProvider { | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved when the action is done. |      * @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(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         // Convenience function to store a data to be synchronized later.
 |         // Convenience function to store a data to be synchronized later.
 | ||||||
|         const storeOffline = (): Promise<any> => { |         const storeOffline = async (): Promise<any> => { | ||||||
|             const action = approve ? 'approve' : 'disapprove'; |             const action = approve ? 'approve' : 'disapprove'; | ||||||
| 
 | 
 | ||||||
|             return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId) |             await this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId); | ||||||
|                     .then(() => { | 
 | ||||||
|             return { |             return { | ||||||
|                 sent: false, |                 sent: false, | ||||||
|             }; |             }; | ||||||
|             }); |  | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         // Get if the opposite action is not synced.
 |         // Get if the opposite action is not synced.
 | ||||||
|         const oppositeAction = approve ? 'disapprove' : 'approve'; |         const oppositeAction = approve ? 'disapprove' : 'approve'; | ||||||
| 
 | 
 | ||||||
|         return this.dataOffline.getEntry(dataId, entryId, oppositeAction, siteId).then(() => { |         const found = await this.deleteEntryOfflineAction(dataId, entryId, oppositeAction, siteId); | ||||||
|             // Found. Just delete the action.
 |         if (found) { | ||||||
|             return this.dataOffline.deleteEntry(dataId, entryId, oppositeAction, siteId); |             // Offline action has been found and deleted. Stop here.
 | ||||||
|         }).catch(() => { |             return; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         if (!this.appProvider.isOnline()) { |         if (!this.appProvider.isOnline()) { | ||||||
|             // App is offline, store the action.
 |             // App is offline, store the action.
 | ||||||
|             return storeOffline(); |             return storeOffline(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             return this.approveEntryOnline(entryId, approve, siteId).then(() => { |         try { | ||||||
|  |             await this.approveEntryOnline(entryId, approve, siteId); | ||||||
|  | 
 | ||||||
|             return { |             return { | ||||||
|                 sent: true, |                 sent: true, | ||||||
|             }; |             }; | ||||||
|             }).catch((error) => { |         } catch (error) { | ||||||
|             if (this.utils.isWebServiceError(error)) { |             if (this.utils.isWebServiceError(error)) { | ||||||
|                 // The WebService has thrown an error, this means that responses cannot be submitted.
 |                 // 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.
 |             // Couldn't connect to server, store in offline.
 | ||||||
|             return storeOffline(); |             return storeOffline(); | ||||||
|             }); |         } | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -297,38 +304,22 @@ export class AddonModDataProvider { | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved when the action is done. |      * @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(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         // Convenience function to store a data to be synchronized later.
 |         // Convenience function to store a data to be synchronized later.
 | ||||||
|         const storeOffline = (): Promise<any> => { |         const storeOffline = async (): Promise<any> => { | ||||||
|             return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId) |             await this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId); | ||||||
|                     .then(() => { | 
 | ||||||
|             return { |             return { | ||||||
|                 sent: false, |                 sent: false, | ||||||
|             }; |             }; | ||||||
|             }); |  | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let justAdded = false; |  | ||||||
| 
 |  | ||||||
|         // Check if the opposite action is not synced and just delete it.
 |         // Check if the opposite action is not synced and just delete it.
 | ||||||
|         return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { |         const addedOffline = await this.deleteEntryOfflineAction(dataId, entryId, 'add', siteId); | ||||||
|             if (entries && entries.length) { |         if (addedOffline) { | ||||||
|                 // Found. Delete other actions first.
 |             // Offline add action found and deleted. Stop here.
 | ||||||
|                 const proms = entries.map((entry) => { |  | ||||||
|                     if (entry.action == 'add') { |  | ||||||
|                         justAdded = true; |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     return this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId); |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
|                 return Promise.all(proms); |  | ||||||
|             } |  | ||||||
|         }).then(() => { |  | ||||||
|             if (justAdded) { |  | ||||||
|                 // The field was added offline, delete and stop.
 |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -337,20 +328,21 @@ export class AddonModDataProvider { | |||||||
|             return storeOffline(); |             return storeOffline(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             return this.deleteEntryOnline(entryId, siteId).then(() => { |         try { | ||||||
|  |             await this.deleteEntryOnline(entryId, siteId); | ||||||
|  | 
 | ||||||
|             return { |             return { | ||||||
|                 sent: true, |                 sent: true, | ||||||
|             }; |             }; | ||||||
|             }).catch((error) => { |         } catch (error) { | ||||||
|             if (this.utils.isWebServiceError(error)) { |             if (this.utils.isWebServiceError(error)) { | ||||||
|                 // The WebService has thrown an error, this means that responses cannot be submitted.
 |                 // 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.
 |             // Couldn't connect to server, store in offline.
 | ||||||
|             return storeOffline(); |             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. |      * Updates an existing entry. | ||||||
|      * |      * | ||||||
| @ -382,82 +397,50 @@ export class AddonModDataProvider { | |||||||
|      * @param forceOffline Force editing entry in offline. |      * @param forceOffline Force editing entry in offline. | ||||||
|      * @return Promise resolved when the action is done. |      * @return Promise resolved when the action is done. | ||||||
|      */ |      */ | ||||||
|     editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, siteId?: string, |     async editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, | ||||||
|             forceOffline: boolean = false): Promise<any> { |             siteId?: string, forceOffline: boolean = false): Promise<any> { | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         // Convenience function to store a data to be synchronized later.
 |         // Convenience function to store a data to be synchronized later.
 | ||||||
|         const storeOffline = (): Promise<any> => { |         const storeOffline = async (): Promise<any> => { | ||||||
|             return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId) |             await this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId); | ||||||
|                     .then(() => { | 
 | ||||||
|             return { |             return { | ||||||
|                 updated: true, |                 updated: true, | ||||||
|                 sent: false, |                 sent: false, | ||||||
|             }; |             }; | ||||||
|             }); |  | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let justAdded = false, |  | ||||||
|             groupId; |  | ||||||
| 
 |  | ||||||
|         if (!this.appProvider.isOnline() || forceOffline) { |         if (!this.appProvider.isOnline() || forceOffline) { | ||||||
|             const notifications = this.checkFields(fields, contents); |             const notifications = this.checkFields(fields, contents); | ||||||
|             if (notifications) { |             if (notifications) { | ||||||
|                 return Promise.resolve({ |                 return { fieldnotifications: notifications }; | ||||||
|                     fieldnotifications: notifications |  | ||||||
|                 }); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Get other not not synced actions.
 |         // Remove unnecessary not synced actions.
 | ||||||
|         return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { |         await this.deleteEntryOfflineAction(dataId, entryId, 'edit', siteId); | ||||||
|             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)); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
|                 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; |  | ||||||
| 
 |  | ||||||
|                     return result; |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|         if (!this.appProvider.isOnline() || forceOffline) { |         if (!this.appProvider.isOnline() || forceOffline) { | ||||||
|             // App is offline, store the action.
 |             // App is offline, store the action.
 | ||||||
|             return storeOffline(); |             return storeOffline(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             return this.editEntryOnline(entryId, contents, siteId).then((result) => { |         try { | ||||||
|  |             const result = await this.editEntryOnline(entryId, contents, siteId); | ||||||
|             result.sent = true; |             result.sent = true; | ||||||
| 
 | 
 | ||||||
|             return result; |             return result; | ||||||
|             }).catch((error) => { |         } catch (error) { | ||||||
|             if (this.utils.isWebServiceError(error)) { |             if (this.utils.isWebServiceError(error)) { | ||||||
|                 // The WebService has thrown an error, this means that responses cannot be submitted.
 |                 // 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.
 |             // Couldn't connect to server, store in offline.
 | ||||||
|             return storeOffline(); |             return storeOffline(); | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|         } |         } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. |      * 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. |      * Performs the whole fetch of the entries in the database. | ||||||
|      * |      * | ||||||
|      * @param dataId Data ID. |      * @param dataId Data ID. | ||||||
|      * @param groupId Group ID. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     fetchAllEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', |     fetchAllEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise<AddonModDataEntry[]> { | ||||||
|             perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, |         options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|             siteId?: string): Promise<AddonModDataEntry[]> { |         options.page = 0; | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |  | ||||||
| 
 | 
 | ||||||
|         return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId); |         return this.fetchEntriesRecursive(dataId, [], options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Recursive call on fetch all entries. |      * Recursive call on fetch all entries. | ||||||
|      * |      * | ||||||
|      * @param dataId Data ID. |      * @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 entries Entries already fetch (just to concatenate them). | ||||||
|      * @param page Page of records to return. |      * @param options Other options. | ||||||
|      * @param siteId Site ID. |  | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected fetchEntriesRecursive(dataId: number, groupId: number, sort: string, order: string, perPage: number, |     protected fetchEntriesRecursive(dataId: number, entries: any, options: AddonModDataGetEntriesOptions = {}) | ||||||
|             forceCache: boolean, ignoreCache: boolean, entries: any, page: number, siteId: string): Promise<AddonModDataEntry[]> { |             : Promise<AddonModDataEntry[]> { | ||||||
|         return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId) |         return this.getEntries(dataId, options).then((result) => { | ||||||
|                 .then((result) => { |  | ||||||
|             entries = entries.concat(result.entries); |             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) { |             if (canLoadMore) { | ||||||
|                 return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, entries, page + 1, |                 options.page++; | ||||||
|                     siteId); | 
 | ||||||
|  |                 return this.fetchEntriesRecursive(dataId, entries, options); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return entries; |             return entries; | ||||||
| @ -557,23 +525,21 @@ export class AddonModDataProvider { | |||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param key Name of the property to check. |      * @param key Name of the property to check. | ||||||
|      * @param value Value to search. |      * @param value Value to search. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param options Other options. | ||||||
|      * @param forceCache True to always get the value from cache, false otherwise. Default false. |  | ||||||
|      * @return Promise resolved when the data is retrieved. |      * @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> { |             Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     courseids: [courseId] |                 courseids: [courseId], | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getDatabaseDataCacheKey(courseId), |                 cacheKey: this.getDatabaseDataCacheKey(courseId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_RARELY |                 updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||||
|  |                 component: AddonModDataProvider.COMPONENT, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
|             if (forceCache) { |  | ||||||
|                 preSets['omitExpires'] = true; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_data_get_databases_by_courses', params, preSets).then((response) => { |             return site.read('mod_data_get_databases_by_courses', params, preSets).then((response) => { | ||||||
|                 if (response && response.databases) { |                 if (response && response.databases) { | ||||||
| @ -593,12 +559,11 @@ export class AddonModDataProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param cmId Course module ID. |      * @param cmId Course module ID. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param options Other options. | ||||||
|      * @param forceCache True to always get the value from cache, false otherwise. Default false. |  | ||||||
|      * @return Promise resolved when the data is retrieved. |      * @return Promise resolved when the data is retrieved. | ||||||
|      */ |      */ | ||||||
|     getDatabase(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise<any> { |     getDatabase(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||||
|         return this.getDatabaseByKey(courseId, 'coursemodule', cmId, siteId, forceCache); |         return this.getDatabaseByKey(courseId, 'coursemodule', cmId, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -606,12 +571,11 @@ export class AddonModDataProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param id Data ID. |      * @param id Data ID. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param options Other options. | ||||||
|      * @param forceCache True to always get the value from cache, false otherwise. Default false. |  | ||||||
|      * @return Promise resolved when the data is retrieved. |      * @return Promise resolved when the data is retrieved. | ||||||
|      */ |      */ | ||||||
|     getDatabaseById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise<any> { |     getDatabaseById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||||
|         return this.getDatabaseByKey(courseId, 'id', id, siteId, forceCache); |         return this.getDatabaseByKey(courseId, 'id', id, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -639,32 +603,23 @@ export class AddonModDataProvider { | |||||||
|      * Get  access information for a given database. |      * Get  access information for a given database. | ||||||
|      * |      * | ||||||
|      * @param dataId Data ID. |      * @param dataId Data ID. | ||||||
|      * @param groupId Group ID. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the database is retrieved. |      * @return Promise resolved when the database is retrieved. | ||||||
|      */ |      */ | ||||||
|     getDatabaseAccessInformation(dataId: number, groupId?: number, offline: boolean = false, ignoreCache: boolean = false, |     getDatabaseAccessInformation(dataId: number, options: AddonModDataAccessInfoOptions = {}): Promise<any> { | ||||||
|             siteId?: string): Promise<any> { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |             options.groupId = options.groupId || 0; | ||||||
|  | 
 | ||||||
|             const params = { |             const params = { | ||||||
|                     databaseid: dataId |                 databaseid: dataId, | ||||||
|                 }, |                 groupid: options.groupId, | ||||||
|                 preSets = { |             }; | ||||||
|                     cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, groupId) |             const preSets = { | ||||||
|  |                 cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, options.groupId), | ||||||
|  |                 component: AddonModDataProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 |  | ||||||
|             if (typeof groupId !== 'undefined') { |  | ||||||
|                 params['groupid'] = groupId; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (offline) { |  | ||||||
|                 preSets['omitExpires'] = true; |  | ||||||
|             } else if (ignoreCache) { |  | ||||||
|                 preSets['getFromCache'] = false; |  | ||||||
|                 preSets['emergencyCache'] = false; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_data_get_data_access_information', params, 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. |      * Get entries for a specific database and group. | ||||||
|      * |      * | ||||||
|      * @param dataId Data ID. |      * @param dataId Data ID. | ||||||
|      * @param groupId Group ID. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the database is retrieved. |      * @return Promise resolved when the database is retrieved. | ||||||
|      */ |      */ | ||||||
|     getEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', page: number = 0, |     getEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise<AddonModDataEntries> { | ||||||
|             perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, |         options.groupId = options.groupId || 0; | ||||||
|             siteId?: string): Promise<AddonModDataEntries> { |         options.sort = options.sort || 0; | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         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).
 |             // Always use sort and order params to improve cache usage (entries are identified by params).
 | ||||||
|             const params = { |             const params = { | ||||||
|                 databaseid: dataId, |                 databaseid: dataId, | ||||||
|                 returncontents: 1, |                 returncontents: 1, | ||||||
|                     page: page, |                 page: options.page, | ||||||
|                     perpage: perPage, |                 perpage: options.perPage, | ||||||
|                     groupid: groupId, |                 groupid: options.groupId, | ||||||
|                     sort: sort, |                 sort: options.sort, | ||||||
|                     order: order |                 order: options.order, | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getEntriesCacheKey(dataId, groupId), |                 cacheKey: this.getEntriesCacheKey(dataId, options.groupId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_SOMETIMES |                 updateFrequency: CoreSite.FREQUENCY_SOMETIMES, | ||||||
|  |                 component: AddonModDataProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|  |                 ...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_data_get_entries', params, preSets).then((response) => { |             return site.read('mod_data_get_entries', params, preSets).then((response) => { | ||||||
|                 response.entries.forEach((entry) => { |                 response.entries.forEach((entry) => { | ||||||
| @ -753,26 +694,23 @@ export class AddonModDataProvider { | |||||||
|      * |      * | ||||||
|      * @param dataId Data ID for caching purposes. |      * @param dataId Data ID for caching purposes. | ||||||
|      * @param entryId Entry ID. |      * @param entryId Entry ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved when the entry is retrieved. |      * @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}> { |              Promise<{entry: AddonModDataEntry, ratinginfo: CoreRatingInfo}> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 entryid: entryId, |                 entryid: entryId, | ||||||
|                     returncontents: 1 |                 returncontents: 1, | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getEntryCacheKey(dataId, entryId), |                 cacheKey: this.getEntryCacheKey(dataId, entryId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_SOMETIMES |                 updateFrequency: CoreSite.FREQUENCY_SOMETIMES, | ||||||
|  |                 component: AddonModDataProvider.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_data_get_entry', params, preSets).then((response) => { |             return site.read('mod_data_get_entry', params, preSets).then((response) => { | ||||||
|                 response.entry.contents = this.utils.arrayToObject(response.entry.contents, 'fieldid'); |                 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. |      * Get the list of configured fields for the given database. | ||||||
|      * |      * | ||||||
|      * @param dataId Data ID. |      * @param dataId Data ID. | ||||||
|      * @param forceCache True to always get the value from cache, false otherwise. Default false. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the fields are retrieved. |      * @return Promise resolved when the fields are retrieved. | ||||||
|      */ |      */ | ||||||
|     getFields(dataId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> { |     getFields(dataId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     databaseid: dataId |                 databaseid: dataId, | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getFieldsCacheKey(dataId), |                 cacheKey: this.getFieldsCacheKey(dataId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_RARELY |                 updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||||
|  |                 component: AddonModDataProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|  |                 ...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_data_get_fields', params, preSets).then((response) => { |             return site.read('mod_data_get_fields', params, preSets).then((response) => { | ||||||
|                 if (response && response.fields) { |                 if (response && response.fields) { | ||||||
| @ -993,46 +925,45 @@ export class AddonModDataProvider { | |||||||
|      * Performs search over a database. |      * Performs search over a database. | ||||||
|      * |      * | ||||||
|      * @param dataId The data instance id. |      * @param dataId The data instance id. | ||||||
|      * @param groupId Group id, 0 means that the function will determine the user group. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the action is done. |      * @return Promise resolved when the action is done. | ||||||
|      */ |      */ | ||||||
|     searchEntries(dataId: number, groupId: number = 0, search?: string, advSearch?: any, sort?: string, order?: string, |     searchEntries(dataId: number, options: AddonModDataSearchEntriesOptions = {}): Promise<AddonModDataEntries> { | ||||||
|             page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise<AddonModDataEntries> { |         options.groupId = options.groupId || 0; | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         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 = { |             const params = { | ||||||
|                 databaseid: dataId, |                 databaseid: dataId, | ||||||
|                     groupid: groupId, |                 groupid: options.groupId, | ||||||
|                 returncontents: 1, |                 returncontents: 1, | ||||||
|                     page: page, |                 page: options.page, | ||||||
|                     perpage: perPage |                 perpage: options.perPage, | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                     getFromCache: false, |                 component: AddonModDataProvider.COMPONENT, | ||||||
|                     saveToCache: true, |                 componentId: options.cmId, | ||||||
|                     emergencyCache: true |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             if (typeof sort != 'undefined') { |             if (typeof options.sort != 'undefined') { | ||||||
|                 params['sort'] = sort; |                 params['sort'] = options.sort; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (typeof order !== 'undefined') { |             if (typeof options.order !== 'undefined') { | ||||||
|                 params['order'] = order; |                 params['order'] = options.order; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (typeof search !== 'undefined') { |             if (typeof options.search !== 'undefined') { | ||||||
|                 params['search'] = search; |                 params['search'] = options.search; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (typeof advSearch !== 'undefined') { |             if (typeof options.advSearch !== 'undefined') { | ||||||
|                 params['advsearch'] = advSearch; |                 params['advsearch'] = options.advSearch; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_data_search_entries', params, preSets).then((response) => { |             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 { CoreSitesProvider } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; |  | ||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | import { CoreCourseProvider } from '@core/course/providers/course'; | ||||||
| import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | ||||||
| import { AddonModDataFieldsDelegate } from './fields-delegate'; | import { AddonModDataFieldsDelegate } from './fields-delegate'; | ||||||
| import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; | 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 { CoreRatingInfo } from '@core/rating/providers/rating'; | ||||||
| import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; | import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; | ||||||
| 
 | 
 | ||||||
| @ -33,12 +34,19 @@ import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; | |||||||
| @Injectable() | @Injectable() | ||||||
| export class AddonModDataHelperProvider { | export class AddonModDataHelperProvider { | ||||||
| 
 | 
 | ||||||
|     constructor(private sitesProvider: CoreSitesProvider, protected dataProvider: AddonModDataProvider, |     constructor( | ||||||
|         private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate, |         protected sitesProvider: CoreSitesProvider, | ||||||
|         private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider, |         protected dataProvider: AddonModDataProvider, | ||||||
|         private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private utils: CoreUtilsProvider, |         protected translate: TranslateService, | ||||||
|         private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, |         protected fieldsDelegate: AddonModDataFieldsDelegate, | ||||||
|         private ratingOffline: CoreRatingOfflineProvider) {} |         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. |      * Returns the record with the offline actions applied. | ||||||
| @ -210,33 +218,21 @@ export class AddonModDataHelperProvider { | |||||||
|      * |      * | ||||||
|      * @param data Database object. |      * @param data Database object. | ||||||
|      * @param fields The fields that define the contents. |      * @param fields The fields that define the contents. | ||||||
|      * @param groupId Group ID. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the database is retrieved. |      * @return Promise resolved when the database is retrieved. | ||||||
|      */ |      */ | ||||||
|     fetchEntries(data: any, fields: any[], groupId: number = 0, search?: string, advSearch?: any[], sort: string = '0', |     fetchEntries(data: any, fields: any[], options: AddonModDataSearchEntriesOptions = {}): Promise<AddonModDataEntries> { | ||||||
|             order: string = 'DESC', page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): |         options.groupId = options.groupId || 0; | ||||||
|             Promise<AddonModDataEntries> { |         options.page = options.page || 0; | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | 
 | ||||||
|  |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const offlineActions = {}; |             const offlineActions = {}; | ||||||
|             const result: AddonModDataEntries = { |             const result: AddonModDataEntries = { | ||||||
|                 entries: [], |                 entries: [], | ||||||
|                 totalcount: 0, |                 totalcount: 0, | ||||||
|                 offlineEntries: [] |                 offlineEntries: [] | ||||||
|             }; |             }; | ||||||
|  |             options.siteId = site.id; | ||||||
| 
 | 
 | ||||||
|             const offlinePromise = this.dataOffline.getDatabaseEntries(data.id, site.id).then((actions) => { |             const offlinePromise = this.dataOffline.getDatabaseEntries(data.id, site.id).then((actions) => { | ||||||
|                 result.hasOfflineActions = !!actions.length; |                 result.hasOfflineActions = !!actions.length; | ||||||
| @ -248,8 +244,8 @@ export class AddonModDataHelperProvider { | |||||||
|                     offlineActions[action.entryid].push(action); |                     offlineActions[action.entryid].push(action); | ||||||
| 
 | 
 | ||||||
|                     // We only display new entries in the first page when not searching.
 |                     // We only display new entries in the first page when not searching.
 | ||||||
|                     if (action.action == 'add' && page == 0 && !search && !advSearch && |                     if (action.action == 'add' && options.page == 0 && !options.search && !options.advSearch && | ||||||
|                             (!action.groupid || !groupId || action.groupid == groupId)) { |                             (!action.groupid || !options.groupId || action.groupid == options.groupId)) { | ||||||
|                         result.offlineEntries.push({ |                         result.offlineEntries.push({ | ||||||
|                             id: action.entryid, |                             id: action.entryid, | ||||||
|                             canmanageentry: true, |                             canmanageentry: true, | ||||||
| @ -275,16 +271,14 @@ export class AddonModDataHelperProvider { | |||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             let fetchPromise: Promise<void>; |             let fetchPromise: Promise<void>; | ||||||
|             if (search || advSearch) { |             if (options.search || options.advSearch) { | ||||||
|                 fetchPromise = this.dataProvider.searchEntries(data.id, groupId, search, advSearch, sort, order, page, perPage, |                 fetchPromise = this.dataProvider.searchEntries(data.id, options).then((fetchResult) => { | ||||||
|                         site.id).then((fetchResult) => { |  | ||||||
|                     result.entries = fetchResult.entries; |                     result.entries = fetchResult.entries; | ||||||
|                     result.totalcount = fetchResult.totalcount; |                     result.totalcount = fetchResult.totalcount; | ||||||
|                     result.maxcount = fetchResult.maxcount; |                     result.maxcount = fetchResult.maxcount; | ||||||
|                 }); |                 }); | ||||||
|             } else { |             } else { | ||||||
|                 fetchPromise = this.dataProvider.getEntries(data.id, groupId, sort, order, page, perPage, false, false, site.id) |                 fetchPromise = this.dataProvider.getEntries(data.id, options).then((fetchResult) => { | ||||||
|                         .then((fetchResult) => { |  | ||||||
|                     result.entries = fetchResult.entries; |                     result.entries = fetchResult.entries; | ||||||
|                     result.totalcount = fetchResult.totalcount; |                     result.totalcount = fetchResult.totalcount; | ||||||
|                 }); |                 }); | ||||||
| @ -324,7 +318,7 @@ export class AddonModDataHelperProvider { | |||||||
| 
 | 
 | ||||||
|                 if (entryId > 0) { |                 if (entryId > 0) { | ||||||
|                     // Online entry.
 |                     // 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  { |                 } else  { | ||||||
|                     // Offline entry or new entry.
 |                     // Offline entry or new entry.
 | ||||||
|                     promise = Promise.resolve({ |                     promise = Promise.resolve({ | ||||||
| @ -644,35 +638,44 @@ export class AddonModDataHelperProvider { | |||||||
|      * @param courseId Course ID. It not defined, it will be fetched. |      * @param courseId Course ID. It not defined, it will be fetched. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @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(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         this.domUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord').then(() => { |         let modal; | ||||||
|             const modal = this.domUtils.showModalLoading(); |         try { | ||||||
|  |             await this.domUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord'); | ||||||
| 
 | 
 | ||||||
|             return this.getActivityCourseIdIfNotSet(dataId, courseId, siteId).then((courseId) => { |             modal = this.domUtils.showModalLoading(); | ||||||
|                 return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId); | 
 | ||||||
|             }).catch((message) => { |             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); |                 this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true); | ||||||
| 
 | 
 | ||||||
|                 return Promise.reject(null); |                 modal && modal.dismiss(); | ||||||
|             }).then(() => { | 
 | ||||||
|                 return this.utils.allPromises([ |                 return; | ||||||
|                     this.dataProvider.invalidateEntryData(dataId, entryId, siteId), |             } | ||||||
|                     this.dataProvider.invalidateEntriesData(dataId, siteId) | 
 | ||||||
|                 ]).catch(() => { |             try { | ||||||
|  |                 await this.dataProvider.invalidateEntryData(dataId, entryId, siteId); | ||||||
|  |                 await this.dataProvider.invalidateEntriesData(dataId, siteId); | ||||||
|  |             } catch (error) { | ||||||
|                 // Ignore errors.
 |                 // Ignore errors.
 | ||||||
|                 }); |             } | ||||||
|             }).then(() => { | 
 | ||||||
|             this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId,  deleted: true}, siteId); |             this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId,  deleted: true}, siteId); | ||||||
| 
 | 
 | ||||||
|             this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); |             this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); | ||||||
|             }).finally(() => { |         } catch (error) { | ||||||
|                 modal.dismiss(); |  | ||||||
|             }); |  | ||||||
|         }).catch(() => { |  | ||||||
|             // Ignore error, it was already displayed.
 |             // 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 { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreGroupsProvider } from '@providers/groups'; | import { CoreGroupsProvider } from '@providers/groups'; | ||||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||||
| import { CoreCommentsProvider } from '@core/comments/providers/comments'; | 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 { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; | ||||||
| import { AddonModDataProvider, AddonModDataEntry } from './data'; | import { AddonModDataProvider, AddonModDataEntry } from './data'; | ||||||
| import { AddonModDataSyncProvider } from './sync'; | import { AddonModDataSyncProvider } from './sync'; | ||||||
| @ -65,16 +65,17 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
|      * |      * | ||||||
|      * @param dataId Database Id. |      * @param dataId Database Id. | ||||||
|      * @param groups Array of groups in the activity. |      * @param groups Array of groups in the activity. | ||||||
|      * @param forceCache True to always get the value from cache, false otherwise. Default false. |      * @param options Other options. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |  | ||||||
|      * @param siteId Site ID. |  | ||||||
|      * @return All unique entries. |      * @return All unique entries. | ||||||
|      */ |      */ | ||||||
|     protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false, |     protected getAllUniqueEntries(dataId: number, groups: any[], options: CoreSitesCommonWSOptions = {}) | ||||||
|             siteId?: string): Promise<AddonModDataEntry[]> { |             : Promise<AddonModDataEntry[]> { | ||||||
|  | 
 | ||||||
|         const promises = groups.map((group) => { |         const promises = groups.map((group) => { | ||||||
|             return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache, |             return this.dataProvider.fetchAllEntries(dataId, { | ||||||
|                 siteId); |                 groupId: group.id, | ||||||
|  |                 ...options, // Include all options.
 | ||||||
|  |             }); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         return Promise.all(promises).then((responses) => { |         return Promise.all(promises).then((responses) => { | ||||||
| @ -96,31 +97,30 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
|      * @param module Module to get the files. |      * @param module Module to get the files. | ||||||
|      * @param courseId Course ID the module belongs to. |      * @param courseId Course ID the module belongs to. | ||||||
|      * @param omitFail True to always return even if fails. Default false. |      * @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 options Other options. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |  | ||||||
|      * @param siteId Site ID. |  | ||||||
|      * @return Promise resolved with the info fetched. |      * @return Promise resolved with the info fetched. | ||||||
|      */ |      */ | ||||||
|     protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, |     protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean, options: CoreCourseCommonModWSOptions = {}) | ||||||
|             ignoreCache: boolean = false, siteId?: string): Promise<any> { |             : Promise<any> { | ||||||
|         let database, |         let database, | ||||||
|             groups = [], |             groups = [], | ||||||
|             entries = [], |             entries = [], | ||||||
|             files = []; |             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); |             files = this.getIntroFilesFromInstance(module, data); | ||||||
|             database = 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) { |                 if (!groupInfo.groups || groupInfo.groups.length == 0) { | ||||||
|                     groupInfo.groups = [{id: 0}]; |                     groupInfo.groups = [{id: 0}]; | ||||||
|                 } |                 } | ||||||
|                 groups = groupInfo.groups; |                 groups = groupInfo.groups; | ||||||
| 
 | 
 | ||||||
|                 return this.getAllUniqueEntries(database.id, groups, forceCache, ignoreCache, siteId); |                 return this.getAllUniqueEntries(database.id, groups, options); | ||||||
|             }); |             }); | ||||||
|         }).then((uniqueEntries) => { |         }).then((uniqueEntries) => { | ||||||
|             entries = uniqueEntries; |             entries = uniqueEntries; | ||||||
| @ -229,8 +229,10 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
|      * @return Promise resolved with true if downloadable, resolved with false otherwise. |      * @return Promise resolved with true if downloadable, resolved with false otherwise. | ||||||
|      */ |      */ | ||||||
|     isDownloadable(module: any, courseId: number): boolean | Promise<boolean> { |     isDownloadable(module: any, courseId: number): boolean | Promise<boolean> { | ||||||
|         return this.dataProvider.getDatabase(courseId, module.id, undefined, true).then((database) => { |         return this.dataProvider.getDatabase(courseId, module.id, { | ||||||
|             return this.dataProvider.getDatabaseAccessInformation(database.id).then((accessData) => { |             readingStrategy: CoreSitesReadingStrategy.PreferCache, | ||||||
|  |         }).then((database) => { | ||||||
|  |             return this.dataProvider.getDatabaseAccessInformation(database.id, {cmId: module.id}).then((accessData) => { | ||||||
|                 // Check if database is restricted by time.
 |                 // Check if database is restricted by time.
 | ||||||
|                 if (!accessData.timeavailable) { |                 if (!accessData.timeavailable) { | ||||||
|                     const time = this.timeUtils.timestamp(); |                     const time = this.timeUtils.timestamp(); | ||||||
| @ -281,23 +283,31 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected prefetchDatabase(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { |     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.
 |             // Prefetch the database data.
 | ||||||
|             const database = info.database, |             const database = info.database, | ||||||
|                 commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(), |                 commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(), | ||||||
|                 promises = []; |                 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)); |             promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id)); | ||||||
| 
 | 
 | ||||||
|             info.groups.forEach((group) => { |             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) => { |             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) { |                 if (commentsEnabled && database.comments) { | ||||||
|                     promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id, |                     promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id, | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreSyncBaseProvider } from '@classes/base-sync'; | import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| @ -188,7 +188,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | |||||||
|             courseId = offlineActions[0].courseid; |             courseId = offlineActions[0].courseid; | ||||||
| 
 | 
 | ||||||
|             // Send the answers.
 |             // Send the answers.
 | ||||||
|             return this.dataProvider.getDatabaseById(courseId, dataId, siteId).then((database) => { |             return this.dataProvider.getDatabaseById(courseId, dataId, {siteId}).then((database) => { | ||||||
|                 data = database; |                 data = database; | ||||||
| 
 | 
 | ||||||
|                 const offlineEntries = {}; |                 const offlineEntries = {}; | ||||||
| @ -208,7 +208,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | |||||||
|             }).then(() => { |             }).then(() => { | ||||||
|                 if (result.updated) { |                 if (result.updated) { | ||||||
|                     // Data has been sent to server. Now invalidate the WS calls.
 |                     // 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.
 |                         // Ignore errors.
 | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
| @ -233,18 +233,23 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | |||||||
|      * @return Promise resolved if success, rejected otherwise. |      * @return Promise resolved if success, rejected otherwise. | ||||||
|      */ |      */ | ||||||
|     protected syncEntry(data: any, entryActions: AddonModDataOfflineAction[], result: any, siteId?: string): Promise<any> { |     protected syncEntry(data: any, entryActions: AddonModDataOfflineAction[], result: any, siteId?: string): Promise<any> { | ||||||
|         let discardError, |         let discardError; | ||||||
|             timePromise, |         let timePromise; | ||||||
|             entryId = entryActions[0].entryid, |         let entryId = entryActions[0].entryid; | ||||||
|             offlineId, |         let offlineId; | ||||||
|             deleted = false; |         let deleted = false; | ||||||
| 
 | 
 | ||||||
|         const editAction = entryActions.find((action) => action.action == 'add' || action.action == 'edit'); |         const editAction = entryActions.find((action) => action.action == 'add' || action.action == 'edit'); | ||||||
|         const approveAction = entryActions.find((action) => action.action == 'approve' || action.action == 'disapprove'); |         const approveAction = entryActions.find((action) => action.action == 'approve' || action.action == 'disapprove'); | ||||||
|         const deleteAction = entryActions.find((action) => action.action == 'delete'); |         const deleteAction = entryActions.find((action) => action.action == 'delete'); | ||||||
|  |         const options = { | ||||||
|  |             cmId: data.coursemodule, | ||||||
|  |             readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||||
|  |             siteId, | ||||||
|  |         }; | ||||||
| 
 | 
 | ||||||
|         if (entryId > 0) { |         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; |                 return entry.entry.timemodified; | ||||||
|             }).catch((error) => { |             }).catch((error) => { | ||||||
|                 if (error && this.utils.isWebServiceError(error)) { |                 if (error && this.utils.isWebServiceError(error)) { | ||||||
| @ -402,7 +407,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | |||||||
|             const promises = []; |             const promises = []; | ||||||
| 
 | 
 | ||||||
|             results.forEach((result) => { |             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) => { |                         .then((data) => { | ||||||
|                     const promises = []; |                     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]="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="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="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-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -184,7 +184,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity | |||||||
|             } |             } | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             // Check if there are answers stored in offline.
 |             // 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) => { |         }).then((accessData) => { | ||||||
|             this.access = accessData; |             this.access = accessData; | ||||||
|             this.showTabs = (accessData.canviewreports || accessData.canviewanalysis) && !accessData.isempty; |             this.showTabs = (accessData.canviewreports || accessData.canviewanalysis) && !accessData.isempty; | ||||||
| @ -220,7 +220,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity | |||||||
|         const promises = []; |         const promises = []; | ||||||
| 
 | 
 | ||||||
|         if (accessData.cancomplete && accessData.cansubmit && accessData.isopen) { |         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; |                 this.goPage = goPage > 0 ? goPage : false; | ||||||
|             })); |             })); | ||||||
|         } |         } | ||||||
| @ -421,7 +421,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity | |||||||
|     setGroup(groupId: number): Promise<any> { |     setGroup(groupId: number): Promise<any> { | ||||||
|         this.group = groupId; |         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.completedCount = analysis.completedcount; | ||||||
|             this.feedback.itemsCount = analysis.itemscount; |             this.feedback.itemsCount = analysis.itemscount; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -65,7 +65,7 @@ export class AddonModFeedbackAttemptPage { | |||||||
|         return this.feedbackProvider.getFeedbackById(this.courseId, this.feedbackId).then((feedback) => { |         return this.feedbackProvider.getFeedbackById(this.courseId, this.feedbackId).then((feedback) => { | ||||||
|             this.feedback = feedback; |             this.feedback = feedback; | ||||||
| 
 | 
 | ||||||
|             return this.feedbackProvider.getItems(this.feedbackId); |             return this.feedbackProvider.getItems(this.feedbackId, {cmId: this.feedback.coursemodule}); | ||||||
|         }).then((items) => { |         }).then((items) => { | ||||||
|             // Add responses and format items.
 |             // Add responses and format items.
 | ||||||
|             this.items = items.items.map((item) => { |             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 { CoreCourseHelperProvider } from '@core/course/providers/helper'; | ||||||
| import { CoreLoginHelperProvider } from '@core/login/providers/helper'; | import { CoreLoginHelperProvider } from '@core/login/providers/helper'; | ||||||
| import { CoreContentLinksHelperProvider } from '@core/contentlinks/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. |  * Page that displays feedback form. | ||||||
| @ -141,6 +141,10 @@ export class AddonModFeedbackFormPage implements OnDestroy { | |||||||
|      */ |      */ | ||||||
|     protected fetchData(): Promise<any> { |     protected fetchData(): Promise<any> { | ||||||
|         this.offline = !this.appProvider.isOnline(); |         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) => { |         return this.feedbackProvider.getFeedback(this.courseId, this.module.id).then((feedbackData) => { | ||||||
|             this.feedback = feedbackData; |             this.feedback = feedbackData; | ||||||
| @ -151,8 +155,7 @@ export class AddonModFeedbackFormPage implements OnDestroy { | |||||||
|         }).then((accessData) => { |         }).then((accessData) => { | ||||||
|             if (!this.preview && accessData.cansubmit && !accessData.isempty) { |             if (!this.preview && accessData.cansubmit && !accessData.isempty) { | ||||||
|                 return typeof this.currentPage == 'undefined' ? |                 return typeof this.currentPage == 'undefined' ? | ||||||
|                     this.feedbackProvider.getResumePage(this.feedback.id, this.offline, true) : |                     this.feedbackProvider.getResumePage(this.feedback.id, options) : Promise.resolve(this.currentPage); | ||||||
|                     Promise.resolve(this.currentPage); |  | ||||||
|             } else { |             } else { | ||||||
|                 this.preview = true; |                 this.preview = true; | ||||||
| 
 | 
 | ||||||
| @ -162,8 +165,9 @@ export class AddonModFeedbackFormPage implements OnDestroy { | |||||||
|             if (!this.offline && !this.utils.isWebServiceError(error)) { |             if (!this.offline && !this.utils.isWebServiceError(error)) { | ||||||
|                 // If it fails, go offline.
 |                 // If it fails, go offline.
 | ||||||
|                 this.offline = true; |                 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); |             return Promise.reject(error); | ||||||
| @ -186,12 +190,18 @@ export class AddonModFeedbackFormPage implements OnDestroy { | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected fetchAccessData(): Promise<any> { |     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 (!this.offline && !this.utils.isWebServiceError(error)) { | ||||||
|                 // If it fails, go offline.
 |                 // If it fails, go offline.
 | ||||||
|                 this.offline = true; |                 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); |             return Promise.reject(error); | ||||||
| @ -203,20 +213,25 @@ export class AddonModFeedbackFormPage implements OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected fetchFeedbackPageData(page: number = 0): Promise<void> { |     protected fetchFeedbackPageData(page: number = 0): Promise<void> { | ||||||
|  |         const options = { | ||||||
|  |             cmId: this.module.id, | ||||||
|  |             readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, | ||||||
|  |         }; | ||||||
|         let promise; |         let promise; | ||||||
|         this.items = []; |         this.items = []; | ||||||
| 
 | 
 | ||||||
|         if (this.preview) { |         if (this.preview) { | ||||||
|             promise = this.feedbackProvider.getItems(this.feedback.id); |             promise = this.feedbackProvider.getItems(this.feedback.id, {cmId: this.module.id}); | ||||||
|         } else { |         } else { | ||||||
|             this.currentPage = page; |             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 (!this.offline && !this.utils.isWebServiceError(error)) { | ||||||
|                     // If it fails, go offline.
 |                     // If it fails, go offline.
 | ||||||
|                     this.offline = true; |                     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); |                 return Promise.reject(error); | ||||||
| @ -262,8 +277,12 @@ export class AddonModFeedbackFormPage implements OnDestroy { | |||||||
|         return this.feedbackSync.syncFeedback(this.feedback.id).catch(() => { |         return this.feedbackSync.syncFeedback(this.feedback.id).catch(() => { | ||||||
|             // Ignore errors.
 |             // Ignore errors.
 | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, goPrevious, formHasErrors, |             return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, { | ||||||
|                     this.courseId).then((response) => { |                 goPrevious, | ||||||
|  |                 formHasErrors, | ||||||
|  |                 courseId: this.courseId, | ||||||
|  |                 cmId: this.module.id, | ||||||
|  |             }).then((response) => { | ||||||
|                 const jumpTo = parseInt(response.jumpto, 10); |                 const jumpTo = parseInt(response.jumpto, 10); | ||||||
| 
 | 
 | ||||||
|                 if (response.completed) { |                 if (response.completed) { | ||||||
|  | |||||||
| @ -111,7 +111,11 @@ export class AddonModFeedbackNonRespondentsPage { | |||||||
|             this.feedbackLoaded = false; |             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; |             this.total = response.total; | ||||||
| 
 | 
 | ||||||
|             if (this.users.length < response.total) { |             if (this.users.length < response.total) { | ||||||
|  | |||||||
| @ -134,7 +134,11 @@ export class AddonModFeedbackRespondentsPage { | |||||||
|             this.feedbackLoaded = false; |             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.responses.total = responses.totalattempts; | ||||||
|             this.anonResponses.total = responses.totalanonattempts; |             this.anonResponses.total = responses.totalanonattempts; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,13 +14,14 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| import { AddonModFeedbackOfflineProvider } from './offline'; | 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. |  * Service that provides some features for feedbacks. | ||||||
| @ -35,7 +36,7 @@ export class AddonModFeedbackProvider { | |||||||
|     static MULTICHOICE_HIDENOSELECT = 'h'; |     static MULTICHOICE_HIDENOSELECT = 'h'; | ||||||
|     static MULTICHOICERATED_VALUE_SEP = '####'; |     static MULTICHOICERATED_VALUE_SEP = '####'; | ||||||
| 
 | 
 | ||||||
|     protected ROOT_CACHE_KEY = this.ROOT_CACHE_KEY + ''; |     protected ROOT_CACHE_KEY = ''; | ||||||
|     protected logger; |     protected logger; | ||||||
| 
 | 
 | ||||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, |     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, | ||||||
| @ -130,13 +131,11 @@ export class AddonModFeedbackProvider { | |||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param items Item to fill the value. |      * @param items Item to fill the value. | ||||||
|      * @param offline True if it should return cached data. Has priority over ignoreCache. |      * @param options Other options. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |  | ||||||
|      * @param siteId Site ID. |  | ||||||
|      * @return Resolved with values when done. |      * @return Resolved with values when done. | ||||||
|      */ |      */ | ||||||
|     protected fillValues(feedbackId: number, items: any[], offline: boolean, ignoreCache: boolean, siteId: string): Promise<any> { |     protected fillValues(feedbackId: number, items: any[], options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId).then((valuesArray) => { |         return this.getCurrentValues(feedbackId, options).then((valuesArray) => { | ||||||
|             const values = {}; |             const values = {}; | ||||||
| 
 | 
 | ||||||
|             valuesArray.forEach((value) => { |             valuesArray.forEach((value) => { | ||||||
| @ -152,7 +151,7 @@ export class AddonModFeedbackProvider { | |||||||
|             // Ignore errors.
 |             // Ignore errors.
 | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             // Merge with offline data.
 |             // Merge with offline data.
 | ||||||
|             return this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).then((offlineValuesArray) => { |             return this.feedbackOffline.getFeedbackResponses(feedbackId, options.siteId).then((offlineValuesArray) => { | ||||||
|                 const offlineValues = {}; |                 const offlineValues = {}; | ||||||
| 
 | 
 | ||||||
|                 // Merge all values into one array.
 |                 // Merge all values into one array.
 | ||||||
| @ -203,24 +202,22 @@ export class AddonModFeedbackProvider { | |||||||
|      * Returns all the feedback non respondents users. |      * Returns all the feedback non respondents users. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param groupId Group id, 0 means that the function will determine the user group. |      * @param options Other options. | ||||||
|      * @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 previous Only for recurrent use. Object with the previous fetched info. |      * @param previous Only for recurrent use. Object with the previous fetched info. | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getAllNonRespondents(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) |     getAllNonRespondents(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise<any> { | ||||||
|             : Promise<any> { |  | ||||||
| 
 | 
 | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|         if (typeof previous == 'undefined') { |         previous = previous || { | ||||||
|             previous = { |  | ||||||
|             page: 0, |             page: 0, | ||||||
|             users: [] |             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) { |             if (previous.users.length < response.total) { | ||||||
|                 previous.users = previous.users.concat(response.users); |                 previous.users = previous.users.concat(response.users); | ||||||
|             } |             } | ||||||
| @ -229,7 +226,7 @@ export class AddonModFeedbackProvider { | |||||||
|                 // Can load more.
 |                 // Can load more.
 | ||||||
|                 previous.page++; |                 previous.page++; | ||||||
| 
 | 
 | ||||||
|                 return this.getAllNonRespondents(feedbackId, groupId, ignoreCache, siteId, previous); |                 return this.getAllNonRespondents(feedbackId, options, previous); | ||||||
|             } |             } | ||||||
|             previous.total = response.total; |             previous.total = response.total; | ||||||
| 
 | 
 | ||||||
| @ -241,25 +238,23 @@ export class AddonModFeedbackProvider { | |||||||
|      * Returns all the feedback user responses. |      * Returns all the feedback user responses. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param groupId Group id, 0 means that the function will determine the user group. |      * @param options Other options. | ||||||
|      * @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 previous Only for recurrent use. Object with the previous fetched info. |      * @param previous Only for recurrent use. Object with the previous fetched info. | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getAllResponsesAnalysis(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) |     getAllResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise<any> { | ||||||
|             : Promise<any> { |  | ||||||
| 
 | 
 | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|         if (typeof previous == 'undefined') { |         previous = previous || { | ||||||
|             previous = { |  | ||||||
|             page: 0, |             page: 0, | ||||||
|             attempts: [], |             attempts: [], | ||||||
|             anonattempts: [] |             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) { |             if (previous.anonattempts.length < responses.totalanonattempts) { | ||||||
|                 previous.anonattempts = previous.anonattempts.concat(responses.anonattempts); |                 previous.anonattempts = previous.anonattempts.concat(responses.anonattempts); | ||||||
|             } |             } | ||||||
| @ -272,7 +267,7 @@ export class AddonModFeedbackProvider { | |||||||
|                 // Can load more.
 |                 // Can load more.
 | ||||||
|                 previous.page++; |                 previous.page++; | ||||||
| 
 | 
 | ||||||
|                 return this.getAllResponsesAnalysis(feedbackId, groupId, ignoreCache, siteId, previous); |                 return this.getAllResponsesAnalysis(feedbackId, options, previous); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             previous.totalattempts = responses.totalattempts; |             previous.totalattempts = responses.totalattempts; | ||||||
| @ -286,27 +281,23 @@ export class AddonModFeedbackProvider { | |||||||
|      * Get analysis information for a given feedback. |      * Get analysis information for a given feedback. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param groupId Group ID. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the feedback is retrieved. |      * @return Promise resolved when the feedback is retrieved. | ||||||
|      */ |      */ | ||||||
|     getAnalysis(feedbackId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise<any> { |     getAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     feedbackid: feedbackId |                 feedbackid: feedbackId, | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getAnalysisDataCacheKey(feedbackId, groupId) |                 cacheKey: this.getAnalysisDataCacheKey(feedbackId, options.groupId), | ||||||
|  |                 component: AddonModFeedbackProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             if (groupId) { |             if (options.groupId) { | ||||||
|                 params['groupid'] = groupId; |                 params['groupid'] = options.groupId; | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (ignoreCache) { |  | ||||||
|                 preSets.getFromCache = false; |  | ||||||
|                 preSets.emergencyCache = false; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_feedback_get_analysis', params, preSets); |             return site.read('mod_feedback_get_analysis', params, preSets); | ||||||
| @ -339,22 +330,23 @@ export class AddonModFeedbackProvider { | |||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param attemptId Attempt id to find. |      * @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 options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @param previous Only for recurrent use. Object with the previous fetched info. |      * @param previous Only for recurrent use. Object with the previous fetched info. | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getAttempt(feedbackId: number, attemptId: number, ignoreCache?: boolean, siteId?: string, previous?: any): Promise<any> { |     getAttempt(feedbackId: number, attemptId: number, options: CoreCourseCommonModWSOptions = {}, previous?: any): Promise<any> { | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|         if (typeof previous == 'undefined') { |         previous = previous || { | ||||||
|             previous = { |  | ||||||
|             page: 0, |             page: 0, | ||||||
|             attemptsLoaded: 0, |             attemptsLoaded: 0, | ||||||
|             anonAttemptsLoaded: 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; |             let attempt; | ||||||
| 
 | 
 | ||||||
|             attempt = responses.attempts.find((attempt) => { |             attempt = responses.attempts.find((attempt) => { | ||||||
| @ -385,7 +377,7 @@ export class AddonModFeedbackProvider { | |||||||
|                 // Can load more. Check there.
 |                 // Can load more. Check there.
 | ||||||
|                 previous.page++; |                 previous.page++; | ||||||
| 
 | 
 | ||||||
|                 return this.getAttempt(feedbackId, attemptId, ignoreCache, siteId, previous); |                 return this.getAttempt(feedbackId, attemptId, options, previous); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Not found and all loaded. Reject.
 |             // Not found and all loaded. Reject.
 | ||||||
| @ -407,23 +399,20 @@ export class AddonModFeedbackProvider { | |||||||
|      * Returns the temporary completion timemodified for the current user. |      * Returns the temporary completion timemodified for the current user. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getCurrentCompletedTimeModified(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise<any> { |     getCurrentCompletedTimeModified(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     feedbackid: feedbackId |                 feedbackid: feedbackId, | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId) |                 cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId), | ||||||
|  |                 component: AddonModFeedbackProvider.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_feedback_get_current_completed_tmp', params, preSets).then((response) => { |             return site.read('mod_feedback_get_current_completed_tmp', params, preSets).then((response) => { | ||||||
|                 if (response && typeof response.feedback != 'undefined' && typeof response.feedback.timemodified != 'undefined') { |                 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. |      * Returns the temporary responses or responses of the last submission for the current user. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param offline True if it should return cached data. Has priority over ignoreCache. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getCurrentValues(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> { |     getCurrentValues(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     feedbackid: feedbackId |                 feedbackid: feedbackId, | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getCurrentValuesDataCacheKey(feedbackId) |                 cacheKey: this.getCurrentValuesDataCacheKey(feedbackId), | ||||||
|  |                 component: AddonModFeedbackProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 |  | ||||||
|             if (offline) { |  | ||||||
|                 preSets['omitExpires'] = true; |  | ||||||
|             } else if (ignoreCache) { |  | ||||||
|                 preSets['getFromCache'] = false; |  | ||||||
|                 preSets['emergencyCache'] = false; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_feedback_get_unfinished_responses', params, preSets).then((response) => { |             return site.read('mod_feedback_get_unfinished_responses', params, preSets).then((response) => { | ||||||
|                 if (!response || typeof response.responses == 'undefined') { |                 if (!response || typeof response.responses == 'undefined') { | ||||||
| @ -508,27 +491,20 @@ export class AddonModFeedbackProvider { | |||||||
|      * Get  access information for a given feedback. |      * Get  access information for a given feedback. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param offline True if it should return cached data. Has priority over ignoreCache. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the feedback is retrieved. |      * @return Promise resolved when the feedback is retrieved. | ||||||
|      */ |      */ | ||||||
|     getFeedbackAccessInformation(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): |     getFeedbackAccessInformation(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|             Promise<any> { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |  | ||||||
|             const params = { |             const params = { | ||||||
|                     feedbackid: feedbackId |                 feedbackid: feedbackId, | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId) |                 cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId), | ||||||
|  |                 component: AddonModFeedbackProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 |  | ||||||
|             if (offline) { |  | ||||||
|                 preSets['omitExpires'] = true; |  | ||||||
|             } else if (ignoreCache) { |  | ||||||
|                 preSets['getFromCache'] = false; |  | ||||||
|                 preSets['emergencyCache'] = false; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_feedback_get_feedback_access_information', params, preSets); |             return site.read('mod_feedback_get_feedback_access_information', params, preSets); | ||||||
|         }); |         }); | ||||||
| @ -570,29 +546,22 @@ export class AddonModFeedbackProvider { | |||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param key Name of the property to check. |      * @param key Name of the property to check. | ||||||
|      * @param value Value to search. |      * @param value Value to search. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param options Other options. | ||||||
|      * @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). |  | ||||||
|      * @return Promise resolved when the feedback is retrieved. |      * @return Promise resolved when the feedback is retrieved. | ||||||
|      */ |      */ | ||||||
|     protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean, |     protected getFeedbackDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) | ||||||
|             ignoreCache?: boolean): Promise<any> { |             : Promise<any> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     courseids: [courseId] |                 courseids: [courseId], | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getFeedbackCacheKey(courseId), |                 cacheKey: this.getFeedbackCacheKey(courseId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_RARELY |                 updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||||
|  |                 component: AddonModFeedbackProvider.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_feedback_get_feedbacks_by_courses', params, preSets).then((response) => { |             return site.read('mod_feedback_get_feedbacks_by_courses', params, preSets).then((response) => { | ||||||
|                 if (response && response.feedbacks) { |                 if (response && response.feedbacks) { | ||||||
| @ -614,13 +583,11 @@ export class AddonModFeedbackProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param cmId Course module ID. |      * @param cmId Course module ID. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param options Other options. | ||||||
|      * @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). |  | ||||||
|      * @return Promise resolved when the feedback is retrieved. |      * @return Promise resolved when the feedback is retrieved. | ||||||
|      */ |      */ | ||||||
|     getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise<any> { |     getFeedback(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||||
|         return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache, ignoreCache); |         return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -628,37 +595,32 @@ export class AddonModFeedbackProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param id Feedback ID. |      * @param id Feedback ID. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param options Other options. | ||||||
|      * @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). |  | ||||||
|      * @return Promise resolved when the feedback is retrieved. |      * @return Promise resolved when the feedback is retrieved. | ||||||
|      */ |      */ | ||||||
|     getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise<any> { |     getFeedbackById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||||
|         return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache, ignoreCache); |         return this.getFeedbackDataByKey(courseId, 'id', id, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns the items (questions) in the given feedback. |      * Returns the items (questions) in the given feedback. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getItems(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise<any> { |     getItems(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     feedbackid: feedbackId |                 feedbackid: feedbackId, | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getItemsDataCacheKey(feedbackId), |                 cacheKey: this.getItemsDataCacheKey(feedbackId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_SOMETIMES |                 updateFrequency: CoreSite.FREQUENCY_SOMETIMES, | ||||||
|  |                 component: AddonModFeedbackProvider.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_feedback_get_items', params, 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. |      * Retrieves a list of students who didn't submit the feedback. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param groupId Group id, 0 means that the function will determine the user group. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, ignoreCache?: boolean, siteId?: string) |     getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> { | ||||||
|             : 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 = { |             const params = { | ||||||
|                 feedbackid: feedbackId, |                 feedbackid: feedbackId, | ||||||
|                     groupid: groupId, |                 groupid: options.groupId, | ||||||
|                     page: page |                 page: options.page, | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, groupId) |                 cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, options.groupId), | ||||||
|  |                 component: AddonModFeedbackProvider.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_feedback_get_non_respondents', params, preSets); |             return site.read('mod_feedback_get_non_respondents', params, preSets); | ||||||
|         }); |         }); | ||||||
| @ -751,25 +709,22 @@ export class AddonModFeedbackProvider { | |||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param page The page to get. |      * @param page The page to get. | ||||||
|      * @param offline True if it should return cached data. Has priority over ignoreCache. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getPageItemsWithValues(feedbackId: number, page: number, offline: boolean = false, ignoreCache: boolean = false, |     getPageItemsWithValues(feedbackId: number, page: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|             siteId?: string): Promise<any> { |         options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |  | ||||||
| 
 | 
 | ||||||
|         return this.getPageItems(feedbackId, page, siteId).then((response) => { |         return this.getPageItems(feedbackId, page, options.siteId).then((response) => { | ||||||
|             return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { |             return this.fillValues(feedbackId, response.items, options).then((items) => { | ||||||
|                 response.items = items; |                 response.items = items; | ||||||
| 
 | 
 | ||||||
|                 return response; |                 return response; | ||||||
|             }); |             }); | ||||||
|         }).catch(() => { |         }).catch(() => { | ||||||
|             // If getPageItems fail we should calculate it using getItems.
 |             // If getPageItems fail we should calculate it using getItems.
 | ||||||
|             return this.getItems(feedbackId, false, siteId).then((response) => { |             return this.getItems(feedbackId, options).then((response) => { | ||||||
|                 return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { |                 return this.fillValues(feedbackId, response.items, options).then((items) => { | ||||||
|                     // Separate items by pages.
 |                     // Separate items by pages.
 | ||||||
|                     let currentPage = 0; |                     let currentPage = 0; | ||||||
|                     const previousPageItems = []; |                     const previousPageItems = []; | ||||||
| @ -819,11 +774,17 @@ export class AddonModFeedbackProvider { | |||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param page Page where we want to jump. |      * @param page Page where we want to jump. | ||||||
|      * @param changePage If page change is forward (1) or backward (-1). |      * @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. |      * @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> { |     protected getPageJumpTo(feedbackId: number, page: number, changePage: number, options: {cmId?: number, siteId?: string}) | ||||||
|         return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => { |             : 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.
 |             // The page we are going has items.
 | ||||||
|             if (resp.items.length > 0) { |             if (resp.items.length > 0) { | ||||||
|                 return page; |                 return page; | ||||||
| @ -831,7 +792,7 @@ export class AddonModFeedbackProvider { | |||||||
| 
 | 
 | ||||||
|             // Check we can jump futher.
 |             // Check we can jump futher.
 | ||||||
|             if ((changePage == 1 && resp.hasnextpage) || (changePage == -1 && resp.hasprevpage)) { |             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.
 |             // Completed or first page.
 | ||||||
| @ -843,27 +804,25 @@ export class AddonModFeedbackProvider { | |||||||
|      * Returns the feedback user responses. |      * Returns the feedback user responses. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param groupId Group id, 0 means that the function will determine the user group. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getResponsesAnalysis(feedbackId: number, groupId: number, page: number, ignoreCache?: boolean, siteId?: string): Promise<any> { |     getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         options.groupId = options.groupId || 0; | ||||||
|  |         options.page = options.page || 0; | ||||||
|  | 
 | ||||||
|  |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 feedbackid: feedbackId, |                 feedbackid: feedbackId, | ||||||
|                     groupid: groupId || 0, |                 groupid: options.groupId, | ||||||
|                     page: page || 0 |                 page: options.page, | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, groupId) |                 cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, options.groupId), | ||||||
|  |                 component: AddonModFeedbackProvider.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_feedback_get_responses_analysis', params, preSets); |             return site.read('mod_feedback_get_responses_analysis', params, preSets); | ||||||
|         }); |         }); | ||||||
| @ -894,26 +853,20 @@ export class AddonModFeedbackProvider { | |||||||
|      * Gets the resume page information. |      * Gets the resume page information. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param offline True if it should return cached data. Has priority over ignoreCache. |      * @param options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getResumePage(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> { |     getResumePage(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     feedbackid: feedbackId |                 feedbackid: feedbackId, | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getResumePageDataCacheKey(feedbackId) |                 cacheKey: this.getResumePageDataCacheKey(feedbackId), | ||||||
|  |                 component: AddonModFeedbackProvider.COMPONENT, | ||||||
|  |                 componentId: options.cmId, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 |  | ||||||
|             if (offline) { |  | ||||||
|                 preSets['omitExpires'] = true; |  | ||||||
|             } else if (ignoreCache) { |  | ||||||
|                 preSets['getFromCache'] = false; |  | ||||||
|                 preSets['emergencyCache'] = false; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_feedback_launch_feedback', params, preSets).then((response) => { |             return site.read('mod_feedback_launch_feedback', params, preSets).then((response) => { | ||||||
|                 if (response && typeof response.gopage != 'undefined') { |                 if (response && typeof response.gopage != 'undefined') { | ||||||
| @ -964,7 +917,7 @@ export class AddonModFeedbackProvider { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Invalidate the prefetched content. |      * Invalidate the prefetched content. | ||||||
|      * To invalidate files, use AddonFeedbackProvider#invalidateFiles. |      * To invalidate files, use AddonModFeedbackProvider#invalidateFiles. | ||||||
|      * |      * | ||||||
|      * @param moduleId The module ID. |      * @param moduleId The module ID. | ||||||
|      * @param courseId Course ID of the module. |      * @param courseId Course ID of the module. | ||||||
| @ -976,7 +929,7 @@ export class AddonModFeedbackProvider { | |||||||
| 
 | 
 | ||||||
|         const promises = []; |         const promises = []; | ||||||
| 
 | 
 | ||||||
|         promises.push(this.getFeedback(courseId, moduleId, siteId).then((feedback) => { |         promises.push(this.getFeedback(courseId, moduleId, {siteId}).then((feedback) => { | ||||||
|             const ps = []; |             const ps = []; | ||||||
| 
 | 
 | ||||||
|             // Do not invalidate module data before getting module info, we need it!
 |             // 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 |      * Returns if feedback has been completed | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     isCompleted(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise<boolean> { |     isCompleted(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<boolean> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     feedbackid: feedbackId |                 feedbackid: feedbackId, | ||||||
|                 }, |             }; | ||||||
|                 preSets: CoreSiteWSPreSets = { |             const preSets = { | ||||||
|                     cacheKey: this.getCompletedDataCacheKey(feedbackId) |                 cacheKey: this.getCompletedDataCacheKey(feedbackId), | ||||||
|  |                 updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||||
|  |                 component: AddonModFeedbackProvider.COMPONENT, | ||||||
|  |                 ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|             }; |             }; | ||||||
| 
 |  | ||||||
|             if (ignoreCache) { |  | ||||||
|                 preSets.getFromCache = false; |  | ||||||
|                 preSets.emergencyCache = false; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return this.utils.promiseWorks(site.read('mod_feedback_get_last_completed', params, 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 feedbackId Feedback ID. | ||||||
|      * @param page The page being processed. |      * @param page The page being processed. | ||||||
|      * @param responses The data to be processed the key is the field name (usually type[index]_id). |      * @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 options Other options. | ||||||
|      * @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. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     processPage(feedbackId: number, page: number, responses: any, goPrevious: boolean, formHasErrors: boolean, courseId: number, |     processPage(feedbackId: number, page: number, responses: any, options: AddonModFeedbackProcessPageOptions = {}): Promise<any> { | ||||||
|             siteId?: string): Promise<any> { |         options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |  | ||||||
| 
 | 
 | ||||||
|         // Convenience function to store a message to be synchronized later.
 |         // Convenience function to store a message to be synchronized later.
 | ||||||
|         const storeOffline = (): Promise<any> => { |         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.
 |                 // Simulate process_page response.
 | ||||||
|                 const response = { |                 const response = { | ||||||
|                         jumpto: page, |                         jumpto: page, | ||||||
| @ -1168,11 +1114,11 @@ export class AddonModFeedbackProvider { | |||||||
|                     }; |                     }; | ||||||
|                 let changePage = 0; |                 let changePage = 0; | ||||||
| 
 | 
 | ||||||
|                 if (goPrevious) { |                 if (options.goPrevious) { | ||||||
|                     if (page > 0) { |                     if (page > 0) { | ||||||
|                         changePage = -1; |                         changePage = -1; | ||||||
|                     } |                     } | ||||||
|                 } else if (!formHasErrors) { |                 } else if (!options.formHasErrors) { | ||||||
|                     // We can only go next if it has no errors.
 |                     // We can only go next if it has no errors.
 | ||||||
|                     changePage = 1; |                     changePage = 1; | ||||||
|                 } |                 } | ||||||
| @ -1181,7 +1127,11 @@ export class AddonModFeedbackProvider { | |||||||
|                     return response; |                     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.
 |                     // Check completion.
 | ||||||
|                     if (changePage == 1 && !resp.hasnextpage) { |                     if (changePage == 1 && !resp.hasnextpage) { | ||||||
|                         response.completed = true; |                         response.completed = true; | ||||||
| @ -1189,7 +1139,7 @@ export class AddonModFeedbackProvider { | |||||||
|                         return response; |                         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) { |                         if (loadPage === false) { | ||||||
|                             // Completed or first page.
 |                             // Completed or first page.
 | ||||||
|                             if (changePage == -1) { |                             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.
 |         // 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.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, options.siteId).then(() => { | ||||||
|             return this.processPageOnline(feedbackId, page, responses, goPrevious, siteId).catch((error) => { |             return this.processPageOnline(feedbackId, page, responses, options.goPrevious, options.siteId).catch((error) => { | ||||||
|                 if (this.utils.isWebServiceError(error)) { |                 if (this.utils.isWebServiceError(error)) { | ||||||
|                     // The WebService has thrown an error, this means that responses cannot be submitted.
 |                     // The WebService has thrown an error, this means that responses cannot be submitted.
 | ||||||
|                     return Promise.reject(error); |                     return Promise.reject(error); | ||||||
| @ -1252,7 +1202,7 @@ export class AddonModFeedbackProvider { | |||||||
|             }).then((response) => { |             }).then((response) => { | ||||||
|                 // Invalidate and update current values because they will change.
 |                 // Invalidate and update current values because they will change.
 | ||||||
|                 return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => { |                 return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => { | ||||||
|                     return this.getCurrentValues(feedbackId, false, false, site.getId()); |                     return this.getCurrentValues(feedbackId, {siteId: site.getId()}); | ||||||
|                 }).catch(() => { |                 }).catch(() => { | ||||||
|                     // Ignore errors.
 |                     // Ignore errors.
 | ||||||
|                 }).then(() => { |                 }).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 { Injectable } from '@angular/core'; | ||||||
| import { NavController, ViewController } from 'ionic-angular'; | import { NavController, ViewController } from 'ionic-angular'; | ||||||
| import { AddonModFeedbackProvider } from './feedback'; | import { AddonModFeedbackProvider, AddonModFeedbackGroupPaginatedOptions } from './feedback'; | ||||||
| import { CoreUserProvider } from '@core/user/providers/user'; | import { CoreUserProvider } from '@core/user/providers/user'; | ||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | import { CoreCourseProvider } from '@core/course/providers/course'; | ||||||
| import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; | 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 { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | 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. |      * Retrieves a list of students who didn't submit the feedback with extra info. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param groupId Group id, 0 means that the function will determine the user group. |      * @param options Other options. | ||||||
|      * @param page The page of records to return. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getNonRespondents(feedbackId: number, groupId: number, page: number): Promise<any> { |     getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> { | ||||||
|         return this.feedbackProvider.getNonRespondents(feedbackId, groupId, page).then((responses) => { |         return this.feedbackProvider.getNonRespondents(feedbackId, options).then((responses) => { | ||||||
|             return this.addImageProfileToAttempts(responses.users).then((users) => { |             return this.addImageProfileToAttempts(responses.users).then((users) => { | ||||||
|                 responses.users = users; |                 responses.users = users; | ||||||
| 
 | 
 | ||||||
| @ -186,12 +185,11 @@ export class AddonModFeedbackHelperProvider { | |||||||
|      * Returns the feedback user responses with extra info. |      * Returns the feedback user responses with extra info. | ||||||
|      * |      * | ||||||
|      * @param feedbackId Feedback ID. |      * @param feedbackId Feedback ID. | ||||||
|      * @param groupId Group id, 0 means that the function will determine the user group. |      * @param options Other options. | ||||||
|      * @param page The page of records to return. |  | ||||||
|      * @return Promise resolved when the info is retrieved. |      * @return Promise resolved when the info is retrieved. | ||||||
|      */ |      */ | ||||||
|     getResponsesAnalysis(feedbackId: number, groupId: number, page: number): Promise<any> { |     getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> { | ||||||
|         return this.feedbackProvider.getResponsesAnalysis(feedbackId, groupId, page).then((responses) => { |         return this.feedbackProvider.getResponsesAnalysis(feedbackId, options).then((responses) => { | ||||||
|             return this.addImageProfileToAttempts(responses.attempts).then((attempts) => { |             return this.addImageProfileToAttempts(responses.attempts).then((attempts) => { | ||||||
|                 responses.attempts = attempts; |                 responses.attempts = attempts; | ||||||
| 
 | 
 | ||||||
| @ -227,7 +225,11 @@ export class AddonModFeedbackHelperProvider { | |||||||
|                 return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackRespondentsPage', stateParams, siteId); |                 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 = { |                 stateParams = { | ||||||
|                     moduleId: module.id, |                     moduleId: module.id, | ||||||
|                     attempt: attempt, |                     attempt: attempt, | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; | |||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | 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. |      * @return Promise resolved with true if downloadable, resolved with false otherwise. | ||||||
|      */ |      */ | ||||||
|     isDownloadable(module: any, courseId: number): boolean | Promise<boolean> { |     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(); |             const now = this.timeUtils.timestamp(); | ||||||
| 
 | 
 | ||||||
|             // Check time first if available.
 |             // Check time first if available.
 | ||||||
| @ -154,7 +156,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH | |||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return this.feedbackProvider.getFeedbackAccessInformation(feedback.id).then((accessData) => { |             return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, {cmId: module.id}).then((accessData) => { | ||||||
|                 return accessData.isopen; |                 return accessData.isopen; | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
| @ -192,15 +194,24 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected prefetchFeedback(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { |     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.
 |         // 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)); |             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 = []; |                 const p2 = []; | ||||||
|                 if (accessData.canedititems || accessData.canviewreports) { |                 if (accessData.canedititems || accessData.canviewreports) { | ||||||
|                     // Get all groups analysis.
 |                     // 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) |                     p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId, true) | ||||||
|                             .then((groupInfo) => { |                             .then((groupInfo) => { | ||||||
|                         const p3 = []; |                         const p3 = []; | ||||||
| @ -209,11 +220,16 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH | |||||||
|                             groupInfo.groups = [{id: 0}]; |                             groupInfo.groups = [{id: 0}]; | ||||||
|                         } |                         } | ||||||
|                         groupInfo.groups.forEach((group) => { |                         groupInfo.groups.forEach((group) => { | ||||||
|                             p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, true, siteId)); |                             const groupOptions = { | ||||||
|                             p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, true, siteId)); |                                 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) { |                             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) => { |                     response.items.forEach((item) => { | ||||||
|                         files = files.concat(item.itemfiles); |                         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(() => { |                     p2.push(this.feedbackProvider.processPageOnline(feedback.id, 0, {}, undefined, siteId).finally(() => { | ||||||
|                         const p4 = []; |                         const p4 = []; | ||||||
| 
 | 
 | ||||||
|                         p4.push(this.feedbackProvider.getCurrentValues(feedback.id, false, true, siteId)); |                         p4.push(this.feedbackProvider.getCurrentValues(feedback.id, modOptions)); | ||||||
|                         p4.push(this.feedbackProvider.getResumePage(feedback.id, false, true, siteId)); |                         p4.push(this.feedbackProvider.getResumePage(feedback.id, modOptions)); | ||||||
| 
 | 
 | ||||||
|                         return Promise.all(p4); |                         return Promise.all(p4); | ||||||
|                     })); |                     })); | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| @ -192,12 +192,12 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv | |||||||
| 
 | 
 | ||||||
|             courseId = responses[0].courseid; |             courseId = responses[0].courseid; | ||||||
| 
 | 
 | ||||||
|             return this.feedbackProvider.getFeedbackById(courseId, feedbackId, siteId).then((feedbackData) => { |             return this.feedbackProvider.getFeedbackById(courseId, feedbackId, {siteId}).then((feedbackData) => { | ||||||
|                 feedback = feedbackData; |                 feedback = feedbackData; | ||||||
| 
 | 
 | ||||||
|                 if (!feedback.multiple_submit) { |                 if (!feedback.multiple_submit) { | ||||||
|                     // If it does not admit multiple submits, check if it is completed to know if we can 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 { |                 } else { | ||||||
|                     return false; |                     return false; | ||||||
|                 } |                 } | ||||||
| @ -220,7 +220,10 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv | |||||||
|                     return Promise.all(promises); |                     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.
 |                     // Sort by page.
 | ||||||
|                     responses.sort((a, b) => { |                     responses.sort((a, b) => { | ||||||
|                         return a.page - b.page; |                         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="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="!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="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-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | import { CoreCourseProvider } from '@core/course/providers/course'; | ||||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| @ -41,11 +41,11 @@ export class AddonModFolderProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param cmId Course module 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. |      * @return Promise resolved when the book is retrieved. | ||||||
|      */ |      */ | ||||||
|     getFolder(courseId: number, cmId: number, siteId?: string): Promise<AddonModFolderFolder> { |     getFolder(courseId: number, cmId: number, options?: CoreSitesCommonWSOptions): Promise<AddonModFolderFolder> { | ||||||
|         return this.getFolderByKey(courseId, 'coursemodule', cmId, siteId); |         return this.getFolderByKey(courseId, 'coursemodule', cmId, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -54,17 +54,20 @@ export class AddonModFolderProvider { | |||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param key Name of the property to check. |      * @param key Name of the property to check. | ||||||
|      * @param value Value to search. |      * @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. |      * @return Promise resolved when the book is retrieved. | ||||||
|      */ |      */ | ||||||
|     protected getFolderByKey(courseId: number, key: string, value: any, siteId?: string): Promise<AddonModFolderFolder> { |     protected getFolderByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |             : Promise<AddonModFolderFolder> { | ||||||
|  |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     courseids: [courseId] |                 courseids: [courseId], | ||||||
|                 }, |             }; | ||||||
|                 preSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getFolderCacheKey(courseId), |                 cacheKey: this.getFolderCacheKey(courseId), | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_RARELY |                 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) |             return site.read('mod_folder_get_folders_by_courses', params, preSets) | ||||||
|  | |||||||
| @ -49,7 +49,7 @@ export class AddonForumDiscussionOptionsMenuComponent implements OnInit { | |||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|         if (this.forumProvider.isSetPinStateAvailableForSite()) { |         if (this.forumProvider.isSetPinStateAvailableForSite()) { | ||||||
|             // Use the canAddDiscussion WS to check if the user can pin discussions.
 |             // 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; |                 this.canPin = !!response.canpindiscussions; | ||||||
|             }).catch(() => { |             }).catch(() => { | ||||||
|                 this.canPin = false; |                 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]="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="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="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-item *ngIf="sortingAvailable" [priority]="300" [content]="'core.sort' | translate" (action)="showSortOrderSelector($event)" iconAction="fa-sort"></core-context-menu-item> | ||||||
|     </core-context-menu> |     </core-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
|  | |||||||
| @ -133,7 +133,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     if (typeof data.deleted != 'undefined' && data.deleted) { |                     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.
 |                             // Discussion deleted, clear details page.
 | ||||||
|                             this.splitviewCtrl.emptyDetails(); |                             this.splitviewCtrl.emptyDetails(); | ||||||
|                         } |                         } | ||||||
| @ -250,7 +250,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|             promises.push(this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => { |             promises.push(this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => { | ||||||
|                 this.usesGroups = (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS); |                 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.
 |                 // 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.
 |                 // 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; |                 const cutoffDateReached = this.forumHelper.isCutoffDateReached(this.forum) && !accessInfo.cancanoverridecutoff; | ||||||
| @ -259,7 +259,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
| 
 | 
 | ||||||
|             if (this.forumProvider.isSetPinStateAvailableForSite()) { |             if (this.forumProvider.isSetPinStateAvailableForSite()) { | ||||||
|                 // Use the canAddDiscussion WS to check if the user can pin discussions.
 |                 // 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; |                     this.canPin = !!response.canpindiscussions; | ||||||
|                 }).catch(() => { |                 }).catch(() => { | ||||||
|                     this.canPin = false; |                     this.canPin = false; | ||||||
| @ -354,7 +354,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|             this.page = 0; |             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; |             let promise; | ||||||
|             if (this.usesGroups) { |             if (this.usesGroups) { | ||||||
|                 promise = this.forumProvider.formatDiscussionsGroups(this.forum.cmid, response.discussions); |                 promise = this.forumProvider.formatDiscussionsGroups(this.forum.cmid, response.discussions); | ||||||
|  | |||||||
| @ -1,12 +1,12 @@ | |||||||
| <core-loading [hideUntil]="loaded" class="core-loading-center"> | <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> |         <ion-icon name="create" item-start></ion-icon> | ||||||
|         <h2>{{ 'addon.mod_forum.edit' | translate }}</h2> |         <h2>{{ 'addon.mod_forum.edit' | translate }}</h2> | ||||||
|     </ion-item> |     </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> |         <ion-icon name="trash" item-start></ion-icon> | ||||||
|         <h2 *ngIf="post.id">{{ 'addon.mod_forum.delete' | translate }}</h2> |         <h2 *ngIf="!offlinePost">{{ 'addon.mod_forum.delete' | translate }}</h2> | ||||||
|         <h2 *ngIf="!post.id">{{ 'core.discard' | translate }}</h2> |         <h2 *ngIf="offlinePost">{{ 'core.discard' | translate }}</h2> | ||||||
|     </ion-item> |     </ion-item> | ||||||
|     <ion-item text-wrap (click)="dismiss()" *ngIf="wordCount"> |     <ion-item text-wrap (click)="dismiss()" *ngIf="wordCount"> | ||||||
|         <h2>{{ 'core.numwords' | translate: {'$a': wordCount} }}</h2> |         <h2>{{ 'core.numwords' | translate: {'$a': wordCount} }}</h2> | ||||||
|  | |||||||
| @ -12,12 +12,14 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit, NgZone } from '@angular/core'; | ||||||
| import { NavParams, ViewController } from 'ionic-angular'; | import { NavParams, ViewController } from 'ionic-angular'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; | ||||||
| import { CoreSite } from '@classes/site'; | import { CoreSite } from '@classes/site'; | ||||||
| import { AddonModForumProvider } from '../../providers/forum'; | 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. |  * This component is meant to display a popover with the post options. | ||||||
| @ -34,45 +36,72 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { | |||||||
|     canDelete = false; |     canDelete = false; | ||||||
|     loaded = false; |     loaded = false; | ||||||
|     url: string; |     url: string; | ||||||
|  |     isOnline: boolean; | ||||||
|  |     offlinePost: boolean; | ||||||
|  | 
 | ||||||
|  |     protected cmId: number; | ||||||
|  |     protected onlineObserver: any; | ||||||
| 
 | 
 | ||||||
|     constructor(navParams: NavParams, |     constructor(navParams: NavParams, | ||||||
|  |             network: Network, | ||||||
|  |             zone: NgZone, | ||||||
|             protected viewCtrl: ViewController, |             protected viewCtrl: ViewController, | ||||||
|             protected domUtils: CoreDomUtilsProvider, |             protected domUtils: CoreDomUtilsProvider, | ||||||
|             protected forumProvider: AddonModForumProvider, |             protected forumProvider: AddonModForumProvider, | ||||||
|             protected sitesProvider: CoreSitesProvider) { |             protected sitesProvider: CoreSitesProvider) { | ||||||
|         this.post = navParams.get('post'); |         this.post = navParams.get('post'); | ||||||
|         this.forumId = navParams.get('forumId'); |         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. |      * Component being initialized. | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     async ngOnInit(): Promise<void> { | ||||||
|         if (this.forumId) { |         if (this.post.id > 0) { | ||||||
|             if (this.post.id) { |  | ||||||
|             const site: CoreSite = this.sitesProvider.getCurrentSite(); |             const site: CoreSite = this.sitesProvider.getCurrentSite(); | ||||||
|                 this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussion}, 'p' + this.post.id); |             this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussionid}, 'p' + this.post.id); | ||||||
| 
 |             this.offlinePost = false; | ||||||
|                 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 { |         } else { | ||||||
|             // Offline post, you can edit or discard the post.
 |             // Offline post, you can edit or discard the post.
 | ||||||
|                 this.canEdit = true; |  | ||||||
|                 this.canDelete = true; |  | ||||||
|             this.loaded = true; |             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 { |             } else { | ||||||
|                 this.loaded = true; |                 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; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Close the popover. |      * Close the popover. | ||||||
|      */ |      */ | ||||||
| @ -84,7 +113,7 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { | |||||||
|      * Delete a post. |      * Delete a post. | ||||||
|      */ |      */ | ||||||
|     deletePost(): void { |     deletePost(): void { | ||||||
|         if (this.post.id) { |         if (!this.offlinePost) { | ||||||
|             this.viewCtrl.dismiss({action: 'delete'}); |             this.viewCtrl.dismiss({action: 'delete'}); | ||||||
|         } else { |         } else { | ||||||
|             this.viewCtrl.dismiss({action: 'deleteoffline'}); |             this.viewCtrl.dismiss({action: 'deleteoffline'}); | ||||||
| @ -95,10 +124,17 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { | |||||||
|      * Edit a post. |      * Edit a post. | ||||||
|      */ |      */ | ||||||
|     editPost(): void { |     editPost(): void { | ||||||
|         if (this.post.id) { |         if (!this.offlinePost) { | ||||||
|             this.viewCtrl.dismiss({action: 'edit'}); |             this.viewCtrl.dismiss({action: 'edit'}); | ||||||
|         } else { |         } else { | ||||||
|             this.viewCtrl.dismiss({action: 'editoffline'}); |             this.viewCtrl.dismiss({action: 'editoffline'}); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component destroyed. | ||||||
|  |      */ | ||||||
|  |     ngOnDestroy(): void { | ||||||
|  |         this.onlineObserver && this.onlineObserver.unsubscribe(); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,11 +3,11 @@ | |||||||
|         <ion-item text-wrap> |         <ion-item text-wrap> | ||||||
|             <div class="addon-mod-forum-post-title" *ngIf="displaySubject"> |             <div class="addon-mod-forum-post-title" *ngIf="displaySubject"> | ||||||
|                 <h2 text-wrap> |                 <h2 text-wrap> | ||||||
|                     <core-icon name="fa-map-pin" *ngIf="post.parent == 0 && post.pinned"></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="post.parent == 0 && !post.pinned && post.starred"></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> |                     <core-format-text [text]="post.subject" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text> | ||||||
|                 </h2> |                 </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> |                     <core-icon name="fa-circle" color="primary"></core-icon> | ||||||
|                 </ion-note> |                 </ion-note> | ||||||
|                 <button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)"> |                 <button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)"> | ||||||
| @ -15,15 +15,15 @@ | |||||||
|                 </button> |                 </button> | ||||||
|             </div> |             </div> | ||||||
|             <div class="addon-mod-forum-post-info"> |             <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"> |                 <div class="addon-mod-forum-post-author"> | ||||||
|                     <h3 *ngIf="post.userfullname">{{post.userfullname}}</h3> |                     <h3 *ngIf="post.author && post.author.fullname">{{post.author.fullname}}</h3> | ||||||
|                     <p *ngIf="post.groupname"><ion-icon name="people"></ion-icon> {{ post.groupname }}</p> |                     <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.modified">{{post.modified * 1000 | coreFormatDate: "strftimerecentfull"}}</p> |                     <p *ngIf="post.timecreated">{{post.timecreated * 1000 | coreFormatDate: "strftimerecentfull"}}</p> | ||||||
|                     <p *ngIf="!post.modified"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p> |                     <p *ngIf="!post.timecreated"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p> | ||||||
|                 </div> |                 </div> | ||||||
|                 <ng-container *ngIf="!displaySubject"> |                 <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> |                         <core-icon name="fa-circle" color="primary"></core-icon> | ||||||
|                     </ion-note> |                     </ion-note> | ||||||
|                     <button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)"> |                     <button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)"> | ||||||
| @ -33,7 +33,7 @@ | |||||||
|             </div> |             </div> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|     </ion-card-header> |     </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"> |         <div padding-bottom *ngIf="post.isprivatereply"> | ||||||
|             <ion-note color="danger">{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note> |             <ion-note color="danger">{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note> | ||||||
|         </div> |         </div> | ||||||
| @ -47,17 +47,17 @@ | |||||||
|             <div item-start>{{ 'core.tag.tags' | translate }}:</div> |             <div item-start>{{ 'core.tag.tags' | translate }}:</div> | ||||||
|             <core-tag-list [tags]="post.tags"></core-tag-list> |             <core-tag-list [tags]="post.tags"></core-tag-list> | ||||||
|         </ion-item> |         </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> |         <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"> |             <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 }} |                 <core-icon name="fa-reply"></core-icon> {{ 'addon.mod_forum.reply' | translate }} | ||||||
|             </button> |             </button> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|     </div> |     </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-item> | ||||||
|             <ion-label stacked>{{ 'addon.mod_forum.subject' | translate }}</ion-label> |             <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> |             <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-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label> | ||||||
|             <ion-checkbox item-end [(ngModel)]="replyData.isprivatereply" name="isprivatereply"></ion-checkbox> |             <ion-checkbox item-end [(ngModel)]="replyData.isprivatereply" name="isprivatereply"></ion-checkbox> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|  |         <ng-container *ngIf="forum.id && forum.maxattachments > 0"> | ||||||
|             <ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable"> |             <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-right" item-start></core-icon> | ||||||
|                 <core-icon *ngIf="advanced" name="fa-caret-down" item-start></core-icon> |                 <core-icon *ngIf="advanced" name="fa-caret-down" item-start></core-icon> | ||||||
|                 {{ 'addon.mod_forum.advanced' | translate }} |                 {{ 'addon.mod_forum.advanced' | translate }} | ||||||
|             </ion-item-divider> |             </ion-item-divider> | ||||||
|             <ng-container *ngIf="advanced"> |             <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> |                 <core-attachments [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments> | ||||||
|  |             </ng-container> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|         <ion-grid> |         <ion-grid> | ||||||
|             <ion-row> |             <ion-row> | ||||||
|  | |||||||
| @ -43,6 +43,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges | |||||||
|     @Input() post: any; // Post.
 |     @Input() post: any; // Post.
 | ||||||
|     @Input() courseId: number; // Post's course ID.
 |     @Input() courseId: number; // Post's course ID.
 | ||||||
|     @Input() discussionId: number; // Post's' discussion 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() component: string; // Component this post belong to.
 | ||||||
|     @Input() componentId: number; // Component ID.
 |     @Input() componentId: number; // Component ID.
 | ||||||
|     @Input() replyData: any; // Object with the new post data. Usually shared between posts.
 |     @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. |      * Component being initialized. | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     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'); |         const reTranslated = this.translate.instant('addon.mod_forum.re'); | ||||||
|         this.displaySubject = !this.parentSubject || |         this.displaySubject = !this.parentSubject || | ||||||
|             (this.post.subject != this.parentSubject && this.post.subject != `Re: ${this.parentSubject}` && |             (this.post.subject != this.parentSubject && this.post.subject != `Re: ${this.parentSubject}` && | ||||||
|                 this.post.subject != `${reTranslated} ${this.parentSubject}`); |                 this.post.subject != `${reTranslated} ${this.parentSubject}`); | ||||||
|         this.defaultReplySubject = (this.post.subject.startsWith('Re: ') || this.post.subject.startsWith(reTranslated)) |         this.defaultReplySubject = this.post.replysubject || ((this.post.subject.startsWith('Re: ') || | ||||||
|             ? this.post.subject : `${reTranslated} ${this.post.subject}`; |             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())); |                     (this.forumProvider.isDeletePostAvailable() || this.forumProvider.isUpdatePostAvailable())); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -192,7 +193,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges | |||||||
| 
 | 
 | ||||||
|         const popover = this.popoverCtrl.create(AddonForumPostOptionsMenuComponent, { |         const popover = this.popoverCtrl.create(AddonForumPostOptionsMenuComponent, { | ||||||
|             post: this.post, |             post: this.post, | ||||||
|             forumId: this.forum.id |             forumId: this.forum.id, | ||||||
|  |             cmId: this.forum.cmid, | ||||||
|         }); |         }); | ||||||
|         popover.onDidDismiss((data) => { |         popover.onDidDismiss((data) => { | ||||||
|             if (data && data.action) { |             if (data && data.action) { | ||||||
| @ -328,7 +330,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges | |||||||
|             this.syncId = this.forumSync.getDiscussionSyncId(this.discussionId); |             this.syncId = this.forumSync.getDiscussionSyncId(this.discussionId); | ||||||
|             this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId); |             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); |                     this.post.isprivatereply); | ||||||
|         }).catch(() => { |         }).catch(() => { | ||||||
|             // Cancelled.
 |             // Cancelled.
 | ||||||
| @ -460,9 +462,9 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges | |||||||
|         this.domUtils.showDeleteConfirm().then(() => { |         this.domUtils.showDeleteConfirm().then(() => { | ||||||
|             const promises = []; |             const promises = []; | ||||||
| 
 | 
 | ||||||
|             promises.push(this.forumOffline.deleteReply(this.post.parent)); |             promises.push(this.forumOffline.deleteReply(this.post.parentid)); | ||||||
|             if (this.forum.id) { |             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.
 |                     // Ignore errors, maybe there are no files.
 | ||||||
|                 })); |                 })); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-navbar core-back-button> |     <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> |         <ion-buttons end> | ||||||
|             <!-- The context menu will be added in here. --> |             <!-- The context menu will be added in here. --> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
| @ -41,14 +41,14 @@ | |||||||
|             <core-icon name="fa-lock"></core-icon> {{ 'addon.mod_forum.discussionlocked' | translate }} |             <core-icon name="fa-lock"></core-icon> {{ 'addon.mod_forum.discussionlocked' | translate }} | ||||||
|         </ion-card> |         </ion-card> | ||||||
| 
 | 
 | ||||||
|         <div *ngIf="discussion" margin-bottom class="highlight"> |         <div *ngIf="startingPost" 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> |             <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> |         </div> | ||||||
| 
 | 
 | ||||||
|         <ion-card *ngIf="sort != 'nested'"> |         <ion-card *ngIf="sort != 'nested'"> | ||||||
|             <ng-container *ngFor="let post of posts; first as first"> |             <ng-container *ngFor="let post of posts; first as first"> | ||||||
|                 <ion-item-divider *ngIf="!first"></ion-item-divider> |                 <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> |             </ng-container> | ||||||
|         </ion-card> |         </ion-card> | ||||||
| 
 | 
 | ||||||
| @ -60,7 +60,7 @@ | |||||||
| 
 | 
 | ||||||
|         <ng-template #nestedPosts let-post="post"> |         <ng-template #nestedPosts let-post="post"> | ||||||
|             <ion-card> |             <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> |             </ion-card> | ||||||
|             <div padding-left *ngIf="post.children.length && post.children[0].subject"> |             <div padding-left *ngIf="post.children.length && post.children[0].subject"> | ||||||
|                 <ng-container *ngFor="let child of post.children"> |                 <ng-container *ngFor="let child of post.children"> | ||||||
|  | |||||||
| @ -52,6 +52,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
|     forum: any = {}; |     forum: any = {}; | ||||||
|     accessInfo: any = {}; |     accessInfo: any = {}; | ||||||
|     discussion: any; |     discussion: any; | ||||||
|  |     startingPost: any; | ||||||
|     posts: any[]; |     posts: any[]; | ||||||
|     discussionLoaded = false; |     discussionLoaded = false; | ||||||
|     postSubjects: { [id: string]: string }; |     postSubjects: { [id: string]: string }; | ||||||
| @ -85,6 +86,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
| 
 | 
 | ||||||
|     protected forumId: number; |     protected forumId: number; | ||||||
|     protected postId: number; |     protected postId: number; | ||||||
|  |     protected parent: number; | ||||||
|     protected onlineObserver: any; |     protected onlineObserver: any; | ||||||
|     protected syncObserver: any; |     protected syncObserver: any; | ||||||
|     protected syncManualObserver: any; |     protected syncManualObserver: any; | ||||||
| @ -120,6 +122,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
|         this.discussionId = this.discussion ? this.discussion.discussion : navParams.get('discussionId'); |         this.discussionId = this.discussion ? this.discussion.discussion : navParams.get('discussionId'); | ||||||
|         this.trackPosts = navParams.get('trackPosts'); |         this.trackPosts = navParams.get('trackPosts'); | ||||||
|         this.postId = navParams.get('postId'); |         this.postId = navParams.get('postId'); | ||||||
|  |         this.parent = navParams.get('parent'); | ||||||
| 
 | 
 | ||||||
|         this.isOnline = this.appProvider.isOnline(); |         this.isOnline = this.appProvider.isOnline(); | ||||||
|         this.onlineObserver = network.onchange().subscribe(() => { |         this.onlineObserver = network.onchange().subscribe(() => { | ||||||
| @ -136,47 +139,67 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
|     /** |     /** | ||||||
|      * View loaded. |      * View loaded. | ||||||
|      */ |      */ | ||||||
|     ionViewDidLoad(): void { |     async ionViewDidLoad(): Promise<void> { | ||||||
|         this.sitesProvider.getCurrentSite().getLocalSiteConfig('AddonModForumDiscussionSort').catch(() => { |         if (this.parent) { | ||||||
|             this.userProvider.getUserPreference('forum_displaymode').catch(() => { |             this.sort = 'nested'; // Force nested order.
 | ||||||
|                 // Ignore errors.
 |         } else { | ||||||
|             }).then((value) => { |             this.sort = await this.getUserSort(); | ||||||
|                 const sortValue = value && parseInt(value, 10); |         } | ||||||
| 
 | 
 | ||||||
|                 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: |                     case 1: | ||||||
|                         this.sort = 'flat-oldest'; |                         return 'flat-oldest'; | ||||||
|                         break; |  | ||||||
|                     case -1: |                     case -1: | ||||||
|                         this.sort = 'flat-newest'; |                         return 'flat-newest'; | ||||||
|                         break; |  | ||||||
|                     case 3: |                     case 3: | ||||||
|                         this.sort = 'nested'; |                         return 'nested'; | ||||||
|                         break; |  | ||||||
|                     case 2: // Threaded not implemented.
 |                     case 2: // Threaded not implemented.
 | ||||||
|                     default: |                     default: | ||||||
|                         // Not set, use default sort.
 |                         // Not set, use default sort.
 | ||||||
|                         // @TODO add fallback to $CFG->forum_displaymode.
 |                         // @TODO add fallback to $CFG->forum_displaymode.
 | ||||||
|                 } |                 } | ||||||
|             }); |             } catch (error) { | ||||||
|         }).then((value) => { |                 // Ignore errors.
 | ||||||
|             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); |  | ||||||
|                     }); |  | ||||||
|             } |             } | ||||||
|             }); |         } | ||||||
|         }); | 
 | ||||||
|  |         return 'flat-oldest'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * User entered the page that contains the component. |      * User entered the page that contains the component. | ||||||
|      */ |      */ | ||||||
|     ionViewDidEnter(): void { |     ionViewDidEnter(): void { | ||||||
|  |         if (this.syncObserver) { | ||||||
|  |             // Already setup.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // Refresh data if this discussion is synchronized automatically.
 |         // Refresh data if this discussion is synchronized automatically.
 | ||||||
|         this.syncObserver = this.eventsProvider.on(AddonModForumSyncProvider.AUTO_SYNCED, (data) => { |         this.syncObserver = this.eventsProvider.on(AddonModForumSyncProvider.AUTO_SYNCED, (data) => { | ||||||
|             if (data.forumId == this.forumId && this.discussionId == data.discussionId |             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 (typeof data.deleted != 'undefined' && data.deleted) { | ||||||
|                         if (data.post.parent == 0) { |                         if (!data.post.parentid) { | ||||||
|                             if (this.svComponent && this.svComponent.isOn()) { |                             if (this.svComponent && this.svComponent.isOn()) { | ||||||
|                                 this.svComponent.emptyDetails(); |                                 this.svComponent.emptyDetails(); | ||||||
|                             } else { |                             } else { | ||||||
| @ -306,9 +329,11 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
|         let ratingInfo; |         let ratingInfo; | ||||||
| 
 | 
 | ||||||
|         return syncPromise.then(() => { |         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; |                 onlinePosts = response.posts; | ||||||
|                 ratingInfo = response.ratinginfo; |                 ratingInfo = response.ratinginfo; | ||||||
|  |                 this.courseId = response.courseid || this.courseId; | ||||||
|  |                 this.forumId = response.forumid || this.forumId; | ||||||
|             }).then(() => { |             }).then(() => { | ||||||
|                 // Check if there are responses stored in offline.
 |                 // Check if there are responses stored in offline.
 | ||||||
|                 return this.forumOffline.getDiscussionReplies(this.discussionId).then((replies) => { |                 return this.forumOffline.getDiscussionReplies(this.discussionId).then((replies) => { | ||||||
| @ -319,7 +344,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
|                     const posts = {}; |                     const posts = {}; | ||||||
|                     onlinePosts.forEach((post) => { |                     onlinePosts.forEach((post) => { | ||||||
|                         posts[post.id] = post; |                         posts[post.id] = post; | ||||||
|                         hasUnreadPosts = hasUnreadPosts || !post.postread; |                         hasUnreadPosts = hasUnreadPosts || !!post.unread; | ||||||
|                     }); |                     }); | ||||||
| 
 | 
 | ||||||
|                     replies.forEach((offlineReply) => { |                     replies.forEach((offlineReply) => { | ||||||
| @ -335,7 +360,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
|                             offlineReplies.push(reply); |                             offlineReplies.push(reply); | ||||||
| 
 | 
 | ||||||
|                             // Disable reply of the parent. Reply in offline to the same post is not allowed, edit instead.
 |                             // 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(() => { |         }).then(() => { | ||||||
|             let posts = offlineReplies.concat(onlinePosts); |             let posts = offlineReplies.concat(onlinePosts); | ||||||
| 
 | 
 | ||||||
|             const startingPost = this.forumProvider.extractStartingPost(posts); |             this.startingPost = this.forumProvider.extractStartingPost(posts); | ||||||
|             if (startingPost) { |  | ||||||
|                 // Update discussion data from first post.
 |  | ||||||
|                 this.discussion = Object.assign(this.discussion || {}, startingPost); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             // If sort type is nested, normal sorting is disabled and nested posts will be displayed.
 |             // If sort type is nested, normal sorting is disabled and nested posts will be displayed.
 | ||||||
|             if (this.sort == 'nested') { |             if (this.sort == 'nested') { | ||||||
|                 // Sort first by creation date to make format tree work.
 |                 // Sort first by creation date to make format tree work.
 | ||||||
|                 this.forumProvider.sortDiscussionPosts(posts, 'ASC'); |                 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 { |             } else { | ||||||
|                 // Set default reply subject.
 |                 // Set default reply subject.
 | ||||||
|                 const direction = this.sort == 'flat-newest' ? 'DESC' : 'ASC'; |                 const direction = this.sort == 'flat-newest' ? 'DESC' : 'ASC'; | ||||||
| @ -381,50 +403,52 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
| 
 | 
 | ||||||
|                 const promises = []; |                 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; |                     this.accessInfo = accessInfo; | ||||||
| 
 | 
 | ||||||
|                     // Disallow replying if cut-off date is reached and the user has not the capability to override it.
 |                     // 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.
 |                     // 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) { |                     if (this.forumHelper.isCutoffDateReached(forum) && !accessInfo.cancanoverridecutoff) { | ||||||
|                         posts.forEach((post) => { |                         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.
 |                 // The discussion object was not passed as parameter and there is no starting post. Should not happen.
 | ||||||
|                 if (!this.discussion) { |                 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); |                 return Promise.all(promises); | ||||||
|             }).catch(() => { |             }).catch(() => { | ||||||
|                 // Ignore errors.
 |                 // Ignore errors.
 | ||||||
|             }).then(() => { |             }).then(() => { | ||||||
| 
 |                 if (!this.discussion && !this.startingPost) { | ||||||
|                 if (!this.discussion) { |  | ||||||
|                     // The discussion object was not passed as parameter and there is no starting post. Should not happen.
 |                     // The discussion object was not passed as parameter and there is no starting post. Should not happen.
 | ||||||
|                     return Promise.reject('Invalid forum discussion.'); |                     return Promise.reject('Invalid forum discussion.'); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (this.discussion.userfullname && this.discussion.parent == 0 && this.forum.type == 'single') { |                 if (this.startingPost.author && this.forum.type == 'single') { | ||||||
|                     // Hide author for first post and type single.
 |                     // Hide author and groups for first post and type single.
 | ||||||
|                     this.discussion.userfullname = null; |                     this.startingPost.author.fullname = null; | ||||||
|  |                     this.startingPost.author.groups = null; | ||||||
|  | 
 | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 this.posts = posts; |                 this.posts = posts; | ||||||
|                 this.ratingInfo = ratingInfo; |                 this.ratingInfo = ratingInfo; | ||||||
|  | 
 | ||||||
|                 this.postSubjects = this.getAllPosts().reduce((postSubjects, post) => { |                 this.postSubjects = this.getAllPosts().reduce((postSubjects, post) => { | ||||||
|                     postSubjects[post.id] = post.subject; |                     postSubjects[post.id] = post.subject; | ||||||
| 
 | 
 | ||||||
|                     return postSubjects; |                     return postSubjects; | ||||||
|                 }, { [this.discussion.id]: this.discussion.subject }); |                 }, { [this.startingPost.id]: this.startingPost.subject }); | ||||||
|             }); |             }); | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             if (this.forumProvider.isSetPinStateAvailableForSite()) { |             if (this.forumProvider.isSetPinStateAvailableForSite()) { | ||||||
|                 // Use the canAddDiscussion WS to check if the user can pin discussions.
 |                 // 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; |                     this.canPin = !!response.canpindiscussions; | ||||||
|                 }).catch(() => { |                 }).catch(() => { | ||||||
|                     this.canPin = false; |                     this.canPin = false; | ||||||
| @ -462,13 +486,14 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
|      * Convenience function to load discussion. |      * Convenience function to load discussion. | ||||||
|      * |      * | ||||||
|      * @param  forumId Forum ID. |      * @param  forumId Forum ID. | ||||||
|  |      * @param  cmId Forum cmid. | ||||||
|      * @param  discussionId Discussion ID. |      * @param  discussionId Discussion ID. | ||||||
|      * @return Promise resolved when done. |      * @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.
 |         // Fetch the discussion if not passed as parameter.
 | ||||||
|         if (!this.discussion && forumId) { |         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.discussion = discussion; | ||||||
|                 this.discussionId = this.discussion.discussion; |                 this.discussionId = this.discussion.discussion; | ||||||
|             }).catch(() => { |             }).catch(() => { | ||||||
| @ -688,6 +713,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
|         this.ratingOfflineObserver && this.ratingOfflineObserver.off(); |         this.ratingOfflineObserver && this.ratingOfflineObserver.off(); | ||||||
|         this.ratingSyncObserver && this.ratingSyncObserver.off(); |         this.ratingSyncObserver && this.ratingSyncObserver.off(); | ||||||
|         this.changeDiscObserver && this.changeDiscObserver.off(); |         this.changeDiscObserver && this.changeDiscObserver.off(); | ||||||
|  |         delete this.syncObserver; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -722,5 +748,4 @@ export class AddonModForumDiscussionPage implements OnDestroy { | |||||||
| 
 | 
 | ||||||
|         return posts; |         return posts; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -112,6 +112,11 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { | |||||||
|      * User entered the page that contains the component. |      * User entered the page that contains the component. | ||||||
|      */ |      */ | ||||||
|     ionViewDidEnter(): void { |     ionViewDidEnter(): void { | ||||||
|  |         if (this.syncObserver) { | ||||||
|  |             // Already setup.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // Refresh data if this discussion is synchronized automatically.
 |         // Refresh data if this discussion is synchronized automatically.
 | ||||||
|         this.syncObserver = this.eventsProvider.on(AddonModForumSyncProvider.AUTO_SYNCED, (data) => { |         this.syncObserver = this.eventsProvider.on(AddonModForumSyncProvider.AUTO_SYNCED, (data) => { | ||||||
|             if (data.forumId == this.forumId && data.userId == this.sitesProvider.getCurrentSiteUserId()) { |             if (data.forumId == this.forumId && data.userId == this.sitesProvider.getCurrentSiteUserId()) { | ||||||
| @ -171,7 +176,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { | |||||||
|                 this.newDiscussion.postToAllGroups = false; |                 this.newDiscussion.postToAllGroups = false; | ||||||
| 
 | 
 | ||||||
|                 // Use the canAddDiscussion WS to check if the user can add attachments and pin discussions.
 |                 // 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.canPin = !!response.canpindiscussions; | ||||||
|                     this.canCreateAttachments = !!response.cancreateattachment; |                     this.canCreateAttachments = !!response.cancreateattachment; | ||||||
|                 }).catch(() => { |                 }).catch(() => { | ||||||
| @ -185,7 +190,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { | |||||||
|             })); |             })); | ||||||
| 
 | 
 | ||||||
|             // Get access information.
 |             // 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; |                 this.accessInfo = accessInfo; | ||||||
|             })); |             })); | ||||||
| 
 | 
 | ||||||
| @ -260,7 +265,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { | |||||||
|      */ |      */ | ||||||
|     protected validateVisibleGroups(forumGroups: any[]): Promise<any[]> { |     protected validateVisibleGroups(forumGroups: any[]): Promise<any[]> { | ||||||
|         // We first check if the user can post to all the groups.
 |         // 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.
 |             // The call failed, let's assume he can't.
 | ||||||
|             return { |             return { | ||||||
|                 status: false, |                 status: false, | ||||||
| @ -280,7 +285,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { | |||||||
|                 const filtered = []; |                 const filtered = []; | ||||||
| 
 | 
 | ||||||
|                 forumGroups.forEach((group) => { |                 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 |                         /* 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. */ |                            it an error will be shown when he tries to add the discussion. */ | ||||||
|                         return { |                         return { | ||||||
| @ -337,7 +342,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { | |||||||
| 
 | 
 | ||||||
|         if (check) { |         if (check) { | ||||||
|             // We need to check if the user can add a discussion to all participants.
 |             // 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.canPin = !!response.canpindiscussions; | ||||||
|                 this.canCreateAttachments = !!response.cancreateattachment; |                 this.canCreateAttachments = !!response.cancreateattachment; | ||||||
| 
 | 
 | ||||||
| @ -549,6 +554,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { | |||||||
|      */ |      */ | ||||||
|     ionViewWillLeave(): void { |     ionViewWillLeave(): void { | ||||||
|         this.syncObserver && this.syncObserver.off(); |         this.syncObserver && this.syncObserver.off(); | ||||||
|  |         delete this.syncObserver; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -60,6 +60,9 @@ export class AddonModForumDiscussionLinkHandler extends CoreContentLinksHandlerB | |||||||
|                 if (data.postid || params.urlHash) { |                 if (data.postid || params.urlHash) { | ||||||
|                     pageParams.postId = parseInt(data.postid || params.urlHash.replace('p', '')); |                     pageParams.postId = parseInt(data.postid || params.urlHash.replace('p', '')); | ||||||
|                 } |                 } | ||||||
|  |                 if (params.parent) { | ||||||
|  |                     pageParams.parent = parseInt(params.parent); | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 this.linkHelper.goInSite(navCtrl, 'AddonModForumDiscussionPage', pageParams, siteId); |                 this.linkHelper.goInSite(navCtrl, 'AddonModForumDiscussionPage', pageParams, siteId); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -14,16 +14,18 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | import { CoreSite } from '@classes/site'; | ||||||
| import { CoreAppProvider } from '@providers/app'; | import { CoreAppProvider } from '@providers/app'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreGroupsProvider } from '@providers/groups'; | 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 { CoreUserProvider } from '@core/user/providers/user'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| import { AddonModForumOfflineProvider } from './offline'; | import { AddonModForumOfflineProvider } from './offline'; | ||||||
| import { CoreRatingInfo } from '@core/rating/providers/rating'; | 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. |  * Service that provides some features for forums. | ||||||
| @ -206,26 +208,29 @@ export class AddonModForumProvider { | |||||||
|      * |      * | ||||||
|      * @param forumId Forum ID. |      * @param forumId Forum ID. | ||||||
|      * @param groupId Group 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: |      * @return Promise resolved with an object with the following properties: | ||||||
|      *         - status (boolean) |      *         - status (boolean) | ||||||
|      *         - canpindiscussions (boolean) |      *         - canpindiscussions (boolean) | ||||||
|      *         - cancreateattachment (boolean) |      *         - cancreateattachment (boolean) | ||||||
|      */ |      */ | ||||||
|     canAddDiscussion(forumId: number, groupId: number, siteId?: string): Promise<any> { |     canAddDiscussion(forumId: number, groupId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         const params = { |         const params = { | ||||||
|             forumid: forumId, |             forumid: forumId, | ||||||
|             groupid: groupId |             groupid: groupId, | ||||||
|         }; |         }; | ||||||
|         const preSets = { |         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) => { |             return site.read('mod_forum_can_add_discussion', params, preSets).then((result) => { | ||||||
|                 if (result) { |                 if (result) { | ||||||
|                     if (typeof result.canpindiscussions == 'undefined') { |                     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; |                         result.canpindiscussions = false; | ||||||
|                     } |                     } | ||||||
|                     if (typeof result.cancreateattachment == 'undefined') { |                     if (typeof result.cancreateattachment == 'undefined') { | ||||||
| @ -245,14 +250,14 @@ export class AddonModForumProvider { | |||||||
|      * Check if a user can post to all groups. |      * Check if a user can post to all groups. | ||||||
|      * |      * | ||||||
|      * @param forumId Forum ID. |      * @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: |      * @return Promise resolved with an object with the following properties: | ||||||
|      *         - status (boolean) |      *         - status (boolean) | ||||||
|      *         - canpindiscussions (boolean) |      *         - canpindiscussions (boolean) | ||||||
|      *         - cancreateattachment (boolean) |      *         - cancreateattachment (boolean) | ||||||
|      */ |      */ | ||||||
|     canAddDiscussionToAll(forumId: number, siteId?: string): Promise<any> { |     canAddDiscussionToAll(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, siteId); |         return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -280,7 +285,7 @@ export class AddonModForumProvider { | |||||||
|      * @return Starting post or undefined if not found. |      * @return Starting post or undefined if not found. | ||||||
|      */ |      */ | ||||||
|     extractStartingPost(posts: any[]): any { |     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; |         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'); |         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. |      * Returns whether or not deletePost WS available or not. | ||||||
|      * |      * | ||||||
| @ -370,17 +387,19 @@ export class AddonModForumProvider { | |||||||
|      * Get all course forums. |      * Get all course forums. | ||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @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. |      * @return Promise resolved when the forums are retrieved. | ||||||
|      */ |      */ | ||||||
|     getCourseForums(courseId: number, siteId?: string): Promise<any[]> { |     getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise<any[]> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                 courseids: [courseId] |                 courseids: [courseId], | ||||||
|             }; |             }; | ||||||
|             const preSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getForumDataCacheKey(courseId), |                 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); |             return site.read('mod_forum_get_forums_by_courses', params, preSets); | ||||||
| @ -393,24 +412,23 @@ export class AddonModForumProvider { | |||||||
|      * @param forumId Forum ID. |      * @param forumId Forum ID. | ||||||
|      * @param discussionId Discussion ID. |      * @param discussionId Discussion ID. | ||||||
|      * @param postId Post ID. |      * @param postId Post ID. | ||||||
|      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved when the post is retrieved. |      * @return Promise resolved when the post is retrieved. | ||||||
|      */ |      */ | ||||||
|     getDiscussionPost(forumId: number, discussionId: number, postId: number, ignoreCache?: boolean, siteId?: string): Promise<any> { |     getDiscussionPost(forumId: number, discussionId: number, postId: number, options: CoreCourseCommonModWSOptions = {}) | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |             : Promise<any> { | ||||||
|             const params = { |  | ||||||
|                     postid: postId |  | ||||||
|                 }, |  | ||||||
|                 preSets: CoreSiteWSPreSets = { |  | ||||||
|                     cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId), |  | ||||||
|                     updateFrequency: CoreSite.FREQUENCY_USUALLY |  | ||||||
|                 }; |  | ||||||
| 
 | 
 | ||||||
|             if (ignoreCache) { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|                 preSets.getFromCache = false; |             const params = { | ||||||
|                 preSets.emergencyCache = false; |                 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) => { |             return site.read('mod_forum_get_discussion_post', params, preSets).then((response) => { | ||||||
|                 if (response.post) { |                 if (response.post) { | ||||||
| @ -427,11 +445,11 @@ export class AddonModForumProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param cmId Course module 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. |      * @return Promise resolved when the forum is retrieved. | ||||||
|      */ |      */ | ||||||
|     getForum(courseId: number, cmId: number, siteId?: string): Promise<any> { |     getForum(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||||
|         return this.getCourseForums(courseId, siteId).then((forums) => { |         return this.getCourseForums(courseId, options).then((forums) => { | ||||||
|             const forum = forums.find((forum) => forum.cmid == cmId); |             const forum = forums.find((forum) => forum.cmid == cmId); | ||||||
|             if (forum) { |             if (forum) { | ||||||
|                 return forum; |                 return forum; | ||||||
| @ -446,11 +464,11 @@ export class AddonModForumProvider { | |||||||
|      * |      * | ||||||
|      * @param courseId Course ID. |      * @param courseId Course ID. | ||||||
|      * @param forumId Forum 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. |      * @return Promise resolved when the forum is retrieved. | ||||||
|      */ |      */ | ||||||
|     getForumById(courseId: number, forumId: number, siteId?: string): Promise<any> { |     getForumById(courseId: number, forumId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> { | ||||||
|         return this.getCourseForums(courseId, siteId).then((forums) => { |         return this.getCourseForums(courseId, options).then((forums) => { | ||||||
|             const forum = forums.find((forum) => forum.id == forumId); |             const forum = forums.find((forum) => forum.id == forumId); | ||||||
|             if (forum) { |             if (forum) { | ||||||
|                 return forum; |                 return forum; | ||||||
| @ -464,24 +482,25 @@ export class AddonModForumProvider { | |||||||
|      * Get access information for a given forum. |      * Get access information for a given forum. | ||||||
|      * |      * | ||||||
|      * @param forumId Forum ID. |      * @param forumId Forum ID. | ||||||
|      * @param forceCache True to always get the value from cache. false otherwise. |      * @param options Other options. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Object with access information. |      * @return Object with access information. | ||||||
|      * @since 3.7 |      * @since 3.7 | ||||||
|      */ |      */ | ||||||
|     getAccessInformation(forumId: number, forceCache?: boolean, siteId?: string): Promise<any> { |     getAccessInformation(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(options.siteId).then((site) => { | ||||||
|             if (!site.wsAvailable('mod_forum_get_forum_access_information')) { |             if (!site.wsAvailable('mod_forum_get_forum_access_information')) { | ||||||
|                 // Access information not available for 3.6 or older sites.
 |                 // Access information not available for 3.6 or older sites.
 | ||||||
|                 return Promise.resolve({}); |                 return Promise.resolve({}); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const params = { |             const params = { | ||||||
|                 forumid: forumId |                 forumid: forumId, | ||||||
|             }; |             }; | ||||||
|             const preSets = { |             const preSets = { | ||||||
|                 cacheKey: this.getAccessInformationCacheKey(forumId), |                 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); |             return site.read('mod_forum_get_forum_access_information', params, preSets); | ||||||
| @ -492,20 +511,91 @@ export class AddonModForumProvider { | |||||||
|      * Get forum discussion posts. |      * Get forum discussion posts. | ||||||
|      * |      * | ||||||
|      * @param discussionId Discussion ID. |      * @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. |      * @return Promise resolved with forum posts and rating info. | ||||||
|      */ |      */ | ||||||
|     getDiscussionPosts(discussionId: number, siteId?: string): Promise<{posts: any[], ratinginfo?: CoreRatingInfo}> { |     getDiscussionPosts(discussionId: number, options: CoreCourseCommonModWSOptions = {}): Promise<{posts: any[], courseid?: number, | ||||||
|         const params = { |             forumid?: number, ratinginfo?: CoreRatingInfo}> { | ||||||
|             discussionid: discussionId | 
 | ||||||
|         }; |         // Convenience function to translate legacy data to new format.
 | ||||||
|         const preSets = { |         const translateLegacyPostsFormat = (posts: any[]): any[] => { | ||||||
|             cacheKey: this.getDiscussionPostsCacheKey(discussionId) |             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, | ||||||
|                 }; |                 }; | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |                 if (post.groupname) { | ||||||
|             return site.read('mod_forum_get_forum_discussion_posts', params, preSets).then((response) => { |                     newPost.author['groups'] = [{name: post.groupname}]; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return newPost; | ||||||
|  |             }); | ||||||
|  |         }; | ||||||
|  |         // 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; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         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 (response) { | ||||||
|  | 
 | ||||||
|  |                     if (wsName == 'mod_forum_get_forum_discussion_posts') { | ||||||
|  |                         response.posts = translateLegacyPostsFormat(response.posts); | ||||||
|  |                     } else { | ||||||
|  |                         response.posts = translateTagsFormatToLegacy(response.posts); | ||||||
|  |                     } | ||||||
|                     this.storeUserData(response.posts); |                     this.storeUserData(response.posts); | ||||||
| 
 | 
 | ||||||
|                     return response; |                     return response; | ||||||
| @ -525,8 +615,13 @@ export class AddonModForumProvider { | |||||||
|     sortDiscussionPosts(posts: any[], direction: string): void { |     sortDiscussionPosts(posts: any[], direction: string): void { | ||||||
|         // @todo: Check children when sorting.
 |         // @todo: Check children when sorting.
 | ||||||
|         posts.sort((a, b) => { |         posts.sort((a, b) => { | ||||||
|             a = parseInt(a.created, 10); |             a = parseInt(a.timecreated, 10) || 0; | ||||||
|             b = parseInt(b.created, 10); |             b = parseInt(b.timecreated, 10) || 0; | ||||||
|  |             if (a == 0 || b == 0) { | ||||||
|  |                 // Leave 0 at the end.
 | ||||||
|  |                 return b - a; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             if (direction == 'ASC') { |             if (direction == 'ASC') { | ||||||
|                 return a - b; |                 return a - b; | ||||||
|             } else { |             } else { | ||||||
| @ -592,32 +687,30 @@ export class AddonModForumProvider { | |||||||
|      * Get forum discussions. |      * Get forum discussions. | ||||||
|      * |      * | ||||||
|      * @param forumId Forum ID. |      * @param forumId Forum ID. | ||||||
|      * @param sortOrder Sort order. |      * @param options Other options. | ||||||
|      * @param page Page. |  | ||||||
|      * @param forceCache True to always get the value from cache. false otherwise. |  | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved with an object with: |      * @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 |      *         - 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. |      *         discussion ID is discussion.discussion. | ||||||
|      *         - canLoadMore: True if there may be more discussions to load. |      *         - canLoadMore: True if there may be more discussions to load. | ||||||
|      */ |      */ | ||||||
|     getDiscussions(forumId: number, sortOrder?: number, page: number = 0, forceCache?: boolean, siteId?: string): Promise<any> { |     getDiscussions(forumId: number, options: AddonModForumGetDiscussionsOptions = {}): Promise<any> { | ||||||
|         sortOrder = sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC; |         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'; |             let method = 'mod_forum_get_forum_discussions_paginated'; | ||||||
|             const params: any = { |             const params: any = { | ||||||
|                 forumid: forumId, |                 forumid: forumId, | ||||||
|                 page: page, |                 page: options.page, | ||||||
|                 perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE |                 perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             if (site.wsAvailable('mod_forum_get_forum_discussions')) { |             if (site.wsAvailable('mod_forum_get_forum_discussions')) { | ||||||
|                 // Since Moodle 3.7.
 |                 // Since Moodle 3.7.
 | ||||||
|                 method = 'mod_forum_get_forum_discussions'; |                 method = 'mod_forum_get_forum_discussions'; | ||||||
|                 params.sortorder = sortOrder; |                 params.sortorder = options.sortOrder; | ||||||
|             } else { |             } else { | ||||||
|                 if (sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { |                 if (options.sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { | ||||||
|                     params.sortby = 'timemodified'; |                     params.sortby = 'timemodified'; | ||||||
|                     params.sortdirection = 'DESC'; |                     params.sortdirection = 'DESC'; | ||||||
|                 } else { |                 } else { | ||||||
| @ -625,29 +718,27 @@ export class AddonModForumProvider { | |||||||
|                     return Promise.reject(null); |                     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) => { |             return site.read(method, params, preSets).catch((error) => { | ||||||
|                 // Try to get the data from cache stored with the old WS method.
 |                 // Try to get the data from cache stored with the old WS method.
 | ||||||
|                 if (!this.appProvider.isOnline() && method == 'mod_forum_get_forum_discussion' && |                 if (!this.appProvider.isOnline() && method == 'mod_forum_get_forum_discussion' && | ||||||
|                         sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { |                         options.sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { | ||||||
| 
 | 
 | ||||||
|                     const params = { |                     const params = { | ||||||
|                         forumid: forumId, |                         forumid: forumId, | ||||||
|                         page: page, |                         page: options.page, | ||||||
|                         perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, |                         perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, | ||||||
|                         sortby: 'timemodified', |                         sortby: 'timemodified', | ||||||
|                         sortdirection: 'DESC' |                         sortdirection: 'DESC' | ||||||
|                     }; |                     }; | ||||||
|                     const preSets: CoreSiteWSPreSets = { |                     Object.assign(preSets, this.sitesProvider.getReadingStrategyPreSets(CoreSitesReadingStrategy.PreferCache)); | ||||||
|                         cacheKey: this.getDiscussionsListCacheKey(forumId, sortOrder), |  | ||||||
|                         omitExpires: true |  | ||||||
|                     }; |  | ||||||
| 
 | 
 | ||||||
|                     return site.read('mod_forum_get_forum_discussions_paginated', params, preSets); |                     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. |      * 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 forumId Forum ID. | ||||||
|  |      * @param cmId Forum cmid. | ||||||
|      * @param sortOrder Sort order. |      * @param sortOrder Sort order. | ||||||
|      * @param forceCache True to always get the value from cache, false otherwise. |      * @param forceCache True to always get the value from cache, false otherwise. | ||||||
|      * @param numPages Number of pages to get. If not defined, all pages. |      * @param numPages Number of pages to get. If not defined, all pages. | ||||||
| @ -682,17 +774,14 @@ export class AddonModForumProvider { | |||||||
|      *         - discussions: List of discussions. |      *         - discussions: List of discussions. | ||||||
|      *         - error: True if an error occurred, false otherwise. |      *         - error: True if an error occurred, false otherwise. | ||||||
|      */ |      */ | ||||||
|     getDiscussionsInPages(forumId: number, sortOrder?: number, forceCache?: boolean, numPages?: number, startPage?: number, |     getDiscussionsInPages(forumId: number, options: AddonModForumGetDiscussionsInPagesOptions = {}): Promise<any> { | ||||||
|             siteId?: string): Promise<any> { |         options.page = options.page || 0; | ||||||
|         if (typeof numPages == 'undefined') { |  | ||||||
|             numPages = -1; |  | ||||||
|         } |  | ||||||
|         startPage = startPage || 0; |  | ||||||
| 
 | 
 | ||||||
|         const result = { |         const result = { | ||||||
|             discussions: [], |             discussions: [], | ||||||
|             error: false |             error: false | ||||||
|         }; |         }; | ||||||
|  |         let numPages = typeof options.numPages == 'undefined' ? -1 : options.numPages; | ||||||
| 
 | 
 | ||||||
|         if (!numPages) { |         if (!numPages) { | ||||||
|             return Promise.resolve(result); |             return Promise.resolve(result); | ||||||
| @ -700,7 +789,7 @@ export class AddonModForumProvider { | |||||||
| 
 | 
 | ||||||
|         const getPage = (page: number): Promise<any> => { |         const getPage = (page: number): Promise<any> => { | ||||||
|             // Get page discussions.
 |             // 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); |                 result.discussions = result.discussions.concat(response.discussions); | ||||||
|                 numPages--; |                 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) => { |             this.getAvailableSortOrders().forEach((sortOrder) => { | ||||||
|                 // We need to get the list of discussions to be able to invalidate their posts.
 |                 // 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.
 |                     // Now invalidate the WS calls.
 | ||||||
|                     const promises = []; |                     const promises = []; | ||||||
| 
 | 
 | ||||||
| @ -1045,6 +1138,16 @@ export class AddonModForumProvider { | |||||||
|         const users = {}; |         const users = {}; | ||||||
| 
 | 
 | ||||||
|         list.forEach((entry) => { |         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); |             const userId = parseInt(entry.userid); | ||||||
|             if (!isNaN(userId) && !users[userId]) { |             if (!isNaN(userId) && !users[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> { |     convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise<any> { | ||||||
|         const reply: any = { |         const reply: any = { | ||||||
|                 attachments: [], |                 id: -offlineReply.timecreated, | ||||||
|                 canreply: false, |                 discussionid: offlineReply.discussionid, | ||||||
|                 children: [], |                 parentid: offlineReply.postid, | ||||||
|                 created: offlineReply.timecreated, |                 hasparent: !!offlineReply.postid, | ||||||
|                 discussion: offlineReply.discussionid, |                 author: { | ||||||
|                 id: false, |                     id: offlineReply.userid, | ||||||
|                 mailed: 0, |                 }, | ||||||
|                 mailnow: 0, |                 timecreated: false, | ||||||
|                 message: offlineReply.message, |  | ||||||
|                 messageformat: 1, |  | ||||||
|                 messagetrust: 0, |  | ||||||
|                 modified: false, |  | ||||||
|                 parent: offlineReply.postid, |  | ||||||
|                 postread: false, |  | ||||||
|                 subject: offlineReply.subject, |                 subject: offlineReply.subject, | ||||||
|                 totalscore: 0, |                 message: offlineReply.message, | ||||||
|                 userid: offlineReply.userid, |                 attachments: [], | ||||||
|                 isprivatereply: offlineReply.options && offlineReply.options.private |                 capabilities: { | ||||||
|  |                     reply: false, | ||||||
|  |                 }, | ||||||
|  |                 unread: false, | ||||||
|  |                 isprivatereply: offlineReply.options && offlineReply.options.private, | ||||||
|  |                 tags: null | ||||||
|             }, |             }, | ||||||
|             promises = []; |             promises = []; | ||||||
| 
 | 
 | ||||||
| @ -187,7 +186,7 @@ export class AddonModForumHelperProvider { | |||||||
|             reply.attachments = offlineReply.options.attachmentsid.online || []; |             reply.attachments = offlineReply.options.attachmentsid.online || []; | ||||||
| 
 | 
 | ||||||
|             if (offlineReply.options.attachmentsid.offline) { |             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) => { |                             .then((files) => { | ||||||
|                     reply.attachments = reply.attachments.concat(files); |                     reply.attachments = reply.attachments.concat(files); | ||||||
|                 })); |                 })); | ||||||
| @ -196,8 +195,8 @@ export class AddonModForumHelperProvider { | |||||||
| 
 | 
 | ||||||
|         // Get user data.
 |         // Get user data.
 | ||||||
|         promises.push(this.userProvider.getProfile(offlineReply.userid, offlineReply.courseid, true).then((user) => { |         promises.push(this.userProvider.getProfile(offlineReply.userid, offlineReply.courseid, true).then((user) => { | ||||||
|             reply.userfullname = user.fullname; |             reply.author.fullname = user.fullname; | ||||||
|             reply.userpictureurl = user.profileimageurl; |             reply.author.urls = { profileimage: user.profileimageurl }; | ||||||
|         }).catch(() => { |         }).catch(() => { | ||||||
|             // Ignore errors.
 |             // 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. |      * This function is inefficient because it needs to fetch all discussion pages in the worst case. | ||||||
|      * |      * | ||||||
|      * @param forumId Forum ID. |      * @param forumId Forum ID. | ||||||
|  |      * @param cmId Forum cmid | ||||||
|      * @param discussionId Discussion ID. |      * @param discussionId Discussion ID. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved with the discussion data. |      * @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(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         const findDiscussion = (page: number): Promise<any> => { |         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) { |                 if (response.discussions && response.discussions.length > 0) { | ||||||
|                     // Note that discussion.id is the main post ID but discussion ID is discussion.discussion.
 |                     // Note that discussion.id is the main post ID but discussion ID is discussion.discussion.
 | ||||||
|                     const discussion = response.discussions.find((discussion) => discussion.discussion == discussionId); |                     const discussion = response.discussions.find((discussion) => discussion.discussion == discussionId); | ||||||
|  | |||||||
| @ -143,7 +143,7 @@ export class AddonModForumModuleHandler implements CoreCourseModuleHandler { | |||||||
| 
 | 
 | ||||||
|         this.forumProvider.invalidateForumData(courseId).finally(() => { |         this.forumProvider.invalidateForumData(courseId).finally(() => { | ||||||
|             // Handle unread posts.
 |             // Handle unread posts.
 | ||||||
|             this.forumProvider.getForum(courseId, moduleId, siteId).then((forumData) => { |             this.forumProvider.getForum(courseId, moduleId, {siteId}).then((forumData) => { | ||||||
|                 data.extraBadgeColor = ''; |                 data.extraBadgeColor = ''; | ||||||
|                 data.extraBadge = forumData.unreadpostscount ? this.translate.instant('addon.mod_forum.unreadpostsnumber', |                 data.extraBadge = forumData.unreadpostscount ? this.translate.instant('addon.mod_forum.unreadpostsnumber', | ||||||
|                     {$a : forumData.unreadpostscount }) : ''; |                     {$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