commit
						7ef37cbebf
					
				
							
								
								
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| node_modules | ||||
| Dockerfile | ||||
							
								
								
									
										8
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| # This file has been retrieved from angular repository. | ||||
| 
 | ||||
| # Auto detect text files and perform LF normalization | ||||
| * text=auto | ||||
| 
 | ||||
| # JS and TS files must always use LF for tools to work | ||||
| *.js eol=lf | ||||
| *.ts eol=lf | ||||
							
								
								
									
										15
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								.travis.yml
									
									
									
									
									
								
							| @ -1,16 +1,23 @@ | ||||
| sudo: required | ||||
| dist: trusty | ||||
| dist: xenial | ||||
| group: edge | ||||
| 
 | ||||
| language: node_js | ||||
| node_js: | ||||
|   - '8.10' | ||||
| node_js: stable | ||||
| 
 | ||||
| before_cache: | ||||
|   - rm -rf $HOME/.cache/electron-builder/wine | ||||
| 
 | ||||
| cache: | ||||
|   directories: | ||||
|     - node_modules | ||||
|     - $HOME/.cache/electron | ||||
|     - $HOME/.cache/electron-builder | ||||
| 
 | ||||
| before_script: | ||||
|   - npm install -g @angular/cli | ||||
|   - npm i npm@latest -g | ||||
|   - gulp | ||||
|   - rm -Rf node_modules/electron-builder-squirrel-windows node_modules/electron-windows-notifications #Avoid electron fail | ||||
| 
 | ||||
| script: | ||||
|   - npm run build | ||||
|  | ||||
							
								
								
									
										27
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| # This image is based on the fat node 11 image. | ||||
| # We require fat images as neither alpine, or slim, include git binaries. | ||||
| FROM node:11 | ||||
| 
 | ||||
| # Port 8100 for ionic dev server. | ||||
| EXPOSE 8100 | ||||
| 
 | ||||
| # Port 35729 is the live-reload server. | ||||
| EXPOSE 35729 | ||||
| 
 | ||||
| # Port 53703 is the Chrome dev logger port. | ||||
| EXPOSE 53703 | ||||
| 
 | ||||
| # MoodleMobile uses Cordova, Ionic, and Gulp. | ||||
| RUN npm install -g cordova ionic gulp && rm -rf /root/.npm | ||||
| 
 | ||||
| WORKDIR /app | ||||
| 
 | ||||
| COPY . /app | ||||
| 
 | ||||
| # The setup script will handle npm installation, cordova setup, and gulp setup. | ||||
| RUN npm run setup && rm -rf /root/.npm | ||||
| 
 | ||||
| # Provide a Healthcheck command for easier use in CI. | ||||
| HEALTHCHECK --interval=10s --timeout=3s --start-period=30s CMD curl -f http://localhost:8100 || exit 1 | ||||
| 
 | ||||
| CMD ["ionic", "serve", "-b"] | ||||
							
								
								
									
										40
									
								
								GoogleService-Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								GoogleService-Info.plist
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| <?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>AD_UNIT_ID_FOR_BANNER_TEST</key> | ||||
| 	<string>ca-app-pub-3940256099942544/2934735716</string> | ||||
| 	<key>AD_UNIT_ID_FOR_INTERSTITIAL_TEST</key> | ||||
| 	<string>ca-app-pub-3940256099942544/4411468910</string> | ||||
| 	<key>CLIENT_ID</key> | ||||
| 	<string>694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr.apps.googleusercontent.com</string> | ||||
| 	<key>REVERSED_CLIENT_ID</key> | ||||
| 	<string>com.googleusercontent.apps.694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr</string> | ||||
| 	<key>API_KEY</key> | ||||
| 	<string>AIzaSyA-77ZjkxII6GV97CC9rdUl83rzdEXu_rM</string> | ||||
| 	<key>GCM_SENDER_ID</key> | ||||
| 	<string>694767596569</string> | ||||
| 	<key>PLIST_VERSION</key> | ||||
| 	<string>1</string> | ||||
| 	<key>BUNDLE_ID</key> | ||||
| 	<string>com.moodle.moodlemobile</string> | ||||
| 	<key>PROJECT_ID</key> | ||||
| 	<string>moodlemobile-push</string> | ||||
| 	<key>STORAGE_BUCKET</key> | ||||
| 	<string>moodlemobile-push.appspot.com</string> | ||||
| 	<key>IS_ADS_ENABLED</key> | ||||
| 	<true></true> | ||||
| 	<key>IS_ANALYTICS_ENABLED</key> | ||||
| 	<false></false> | ||||
| 	<key>IS_APPINVITE_ENABLED</key> | ||||
| 	<true></true> | ||||
| 	<key>IS_GCM_ENABLED</key> | ||||
| 	<true></true> | ||||
| 	<key>IS_SIGNIN_ENABLED</key> | ||||
| 	<true></true> | ||||
| 	<key>GOOGLE_APP_ID</key> | ||||
| 	<string>1:694767596569:ios:a4cdad4d168c9d1a</string> | ||||
| 	<key>DATABASE_URL</key> | ||||
| 	<string>https://moodlemobile-push.firebaseio.com</string> | ||||
| </dict> | ||||
| </plist> | ||||
							
								
								
									
										29
									
								
								config.xml
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								config.xml
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <widget id="com.moodle.moodlemobile" version="3.6.0" 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 id="com.moodle.moodlemobile" version="3.6.1" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0"> | ||||
|     <name>Moodle</name> | ||||
|     <description>Moodle official app</description> | ||||
|     <author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author> | ||||
| @ -41,6 +41,7 @@ | ||||
|         <param name="ios-package" onload="true" value="CDVStatusBar" /> | ||||
|     </feature> | ||||
|     <platform name="android"> | ||||
|         <resource-file src="google-services.json" target="app/google-services.json" /> | ||||
|         <splash qualifier="land-ldpi" src="resources/android/splash/drawable-land-ldpi-screen.png" /> | ||||
|         <splash qualifier="land-mdpi" src="resources/android/splash/drawable-land-mdpi-screen.png" /> | ||||
|         <splash qualifier="land-hdpi" src="resources/android/splash/drawable-land-hdpi-screen.png" /> | ||||
| @ -59,8 +60,25 @@ | ||||
|         <icon density="xhdpi" src="resources/android/icon/drawable-xhdpi-icon.png" /> | ||||
|         <icon density="xxhdpi" src="resources/android/icon/drawable-xxhdpi-icon.png" /> | ||||
|         <icon density="xxxhdpi" src="resources/android/icon/drawable-xxxhdpi-icon.png" /> | ||||
|         <resource-file src="resources/android/icon/drawable-ldpi-smallicon.png" target="app/src/main/res/mipmap-ldpi/smallicon.png" /> | ||||
|         <resource-file src="resources/android/icon/drawable-mdpi-smallicon.png" target="app/src/main/res/mipmap-mdpi/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/splash/drawable-land-hdpi-screen.png" target="app/src/main/res/drawable-land-hdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-land-ldpi-screen.png" target="app/src/main/res/drawable-land-ldpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-land-mdpi-screen.png" target="app/src/main/res/drawable-land-mdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-land-xhdpi-screen.png" target="app/src/main/res/drawable-land-xhdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-land-xxhdpi-screen.png" target="app/src/main/res/drawable-land-xxhdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-land-xxxhdpi-screen.png" target="app/src/main/res/drawable-land-xxxhdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-port-hdpi-screen.png" target="app/src/main/res/drawable-port-hdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-port-ldpi-screen.png" target="app/src/main/res/drawable-port-ldpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-port-mdpi-screen.png" target="app/src/main/res/drawable-port-mdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-port-xhdpi-screen.png" target="app/src/main/res/drawable-port-xhdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-port-xxhdpi-screen.png" target="app/src/main/res/drawable-port-xxhdpi/screen.png" /> | ||||
|         <resource-file src="resources/android/splash/drawable-port-xxxhdpi-screen.png" target="app/src/main/res/drawable-port-xxxhdpi/screen.png" /> | ||||
|     </platform> | ||||
|     <platform name="ios"> | ||||
|         <resource-file src="GoogleService-Info.plist" /> | ||||
|         <icon height="57" src="resources/ios/icon/icon.png" width="57" /> | ||||
|         <icon height="114" src="resources/ios/icon/icon@2x.png" width="114" /> | ||||
|         <icon height="40" src="resources/ios/icon/icon-40.png" width="40" /> | ||||
| @ -94,7 +112,7 @@ | ||||
|         <icon height="1024" src="resources/ios/icon/icon-1024.png" width="1024" /> | ||||
|         <splash height="2732" src="resources/ios/splash/Default@2x~universal~anyany.png" width="2732" /> | ||||
|     </platform> | ||||
|     <plugin name="com-darryncampbell-cordova-plugin-intent" spec="1.1.1" /> | ||||
|     <plugin name="com-darryncampbell-cordova-plugin-intent" spec="1.1.5" /> | ||||
|     <plugin name="cordova-android-support-gradle-release" spec="2.0.1"> | ||||
|         <variable name="ANDROID_SUPPORT_VERSION" value="27.1.0" /> | ||||
|     </plugin> | ||||
| @ -111,7 +129,7 @@ | ||||
|     <plugin name="cordova-plugin-globalization" spec="1.11.0" /> | ||||
|     <plugin name="cordova-plugin-inappbrowser" spec="3.0.0" /> | ||||
|     <plugin name="cordova-plugin-ionic-keyboard" spec="2.1.3" /> | ||||
|     <plugin name="cordova-plugin-local-notifications-mm" spec="1.0.13" /> | ||||
|     <plugin name="cordova-plugin-local-notification" spec="0.9.0-beta.3" /> | ||||
|     <plugin name="cordova-plugin-media-capture" spec="3.0.2" /> | ||||
|     <plugin name="cordova-plugin-network-information" spec="2.0.1" /> | ||||
|     <plugin name="cordova-plugin-screen-orientation" spec="3.0.1" /> | ||||
| @ -121,8 +139,9 @@ | ||||
|     <plugin name="cordova-plugin-zip" spec="3.1.0" /> | ||||
|     <plugin name="cordova-sqlite-storage" spec="2.6.0" /> | ||||
|     <plugin name="nl.kingsquare.cordova.background-audio" spec="1.0.1" /> | ||||
|     <plugin name="phonegap-plugin-push" spec="https://github.com/moodlemobile/phonegap-plugin-push.git#moodle"> | ||||
|         <variable name="SENDER_ID" value="694767596569" /> | ||||
|     <plugin name="phonegap-plugin-push" spec="https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v2"> | ||||
|         <variable name="ANDROID_SUPPORT_V13_VERSION" value="27.+" /> | ||||
|         <variable name="FCM_VERSION" value="17.0.+" /> | ||||
|     </plugin> | ||||
|     <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" /> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| // New copy task for font files
 | ||||
| // New copy task for font files and config.json.
 | ||||
| module.exports = { | ||||
|   // Override Ionic copyFonts task to exclude Roboto and Noto fonts.
 | ||||
|   copyFonts: { | ||||
| @ -8,5 +8,9 @@ module.exports = { | ||||
|   copyFontAwesome: { | ||||
|     src: ['{{ROOT}}/node_modules/font-awesome/fonts/**/*'], | ||||
|     dest: '{{WWW}}/assets/fonts' | ||||
|   }, | ||||
|   copyConfig: { | ||||
|     src: ['{{ROOT}}/src/config.json'], | ||||
|     dest: '{{WWW}}/' | ||||
|   } | ||||
| }; | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|   <Identity Name="3312ADB7.MoodleDesktop" | ||||
|     ProcessorArchitecture="x64" | ||||
|     Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6" | ||||
|     Version="3.6.0.0" /> | ||||
|     Version="3.6.1.0" /> | ||||
|   <Properties> | ||||
|     <DisplayName>Moodle Desktop</DisplayName> | ||||
|     <PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName> | ||||
|  | ||||
| @ -70,6 +70,26 @@ function createWindow() { | ||||
|     mainWindow.webContents.setUserAgent(mainWindow.webContents.getUserAgent() + ' ' + userAgent); | ||||
| } | ||||
| 
 | ||||
| // Make sure that only a single instance of the app is running.
 | ||||
| // For some reason, gotTheLock is always false in signed Mac apps so we should ingore it.
 | ||||
| // See https://github.com/electron/electron/issues/15958
 | ||||
| var gotTheLock = app.requestSingleInstanceLock(); | ||||
| 
 | ||||
| if (!gotTheLock && os.platform().indexOf('darwin') == -1) { | ||||
|     // It's not the main instance of the app, kill it.
 | ||||
|     app.exit(); | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| app.on('second-instance', (event, commandLine, workingDirectory) => { | ||||
|     // Another instance was launched. If it was launched with a URL, it should be in the second param.
 | ||||
|     if (commandLine && commandLine[1]) { | ||||
|         appLaunched(commandLine[1]); | ||||
|     } else { | ||||
|         focusApp(); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| // This method will be called when Electron has finished initialization and is ready to create browser windows.
 | ||||
| // Some APIs can only be used after this event occurs.
 | ||||
| app.on('ready', function() { | ||||
| @ -122,23 +142,6 @@ fs.readFile(path.join(__dirname, 'config.json'), 'utf8', (err, data) => { | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| // Make sure that only a single instance of the app is running.
 | ||||
| var shouldQuit = app.makeSingleInstance((argv, workingDirectory) => { | ||||
|     // Another instance was launched. If it was launched with a URL, it should be in the second param.
 | ||||
|     if (argv && argv[1]) { | ||||
|         appLaunched(argv[1]); | ||||
|     } else { | ||||
|         focusApp(); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| // For some reason, shouldQuit is always true in signed Mac apps so we should ingore it.
 | ||||
| if (shouldQuit && os.platform().indexOf('darwin') == -1) { | ||||
|     // It's not the main instance of the app, kill it.
 | ||||
|     app.exit(); | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| // Listen for open-url events (Mac OS only).
 | ||||
| app.on('open-url', (event, url) => { | ||||
|     event.preventDefault(); | ||||
|  | ||||
							
								
								
									
										45
									
								
								google-services.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								google-services.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| { | ||||
|   "project_info": { | ||||
|     "project_number": "694767596569", | ||||
|     "firebase_url": "https://moodlemobile-push.firebaseio.com", | ||||
|     "project_id": "moodlemobile-push", | ||||
|     "storage_bucket": "moodlemobile-push.appspot.com" | ||||
|   }, | ||||
|   "client": [ | ||||
|     { | ||||
|       "client_info": { | ||||
|         "mobilesdk_app_id": "1:694767596569:android:a4cdad4d168c9d1a", | ||||
|         "android_client_info": { | ||||
|           "package_name": "com.moodle.moodlemobile" | ||||
|         } | ||||
|       }, | ||||
|       "oauth_client": [ | ||||
|         { | ||||
|           "client_id": "694767596569-icveqqa2n56oh44l6ev1dr2oh67nh8il.apps.googleusercontent.com", | ||||
|           "client_type": 3 | ||||
|         } | ||||
|       ], | ||||
|       "api_key": [ | ||||
|         { | ||||
|           "current_key": "AIzaSyCb2zogu0P_aZ2LNgdwzshWExITPKTXJyk" | ||||
|         }, | ||||
|         { | ||||
|           "current_key": "AIzaSyDRT1HwT0gSsTty0whOVtoNKAh8SPrJXLE" | ||||
|         } | ||||
|       ], | ||||
|       "services": { | ||||
|         "analytics_service": { | ||||
|           "status": 1 | ||||
|         }, | ||||
|         "appinvite_service": { | ||||
|           "status": 1, | ||||
|           "other_platform_oauth_client": [] | ||||
|         }, | ||||
|         "ads_service": { | ||||
|           "status": 2 | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "configuration_version": "1" | ||||
| } | ||||
							
								
								
									
										10
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								gulpfile.js
									
									
									
									
									
								
							| @ -47,7 +47,13 @@ function treatFile(file, data) { | ||||
|         return; // ignore
 | ||||
|     } | ||||
|     try { | ||||
|         var path = file.path.substr(file.path.lastIndexOf('/src/') + 5); | ||||
|         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); | ||||
| @ -65,7 +71,7 @@ function treatMergedData(data) { | ||||
|     var mergedOrdered = {}; | ||||
| 
 | ||||
|     for (var filepath in data) { | ||||
|         var pathSplit = filepath.split('/'), | ||||
|         var pathSplit = filepath.split(/[\/\\]/), | ||||
|             prefix; | ||||
| 
 | ||||
|         pathSplit.pop(); | ||||
|  | ||||
| @ -1,49 +0,0 @@ | ||||
| #!/usr/bin/env node
 | ||||
| 
 | ||||
| // This hook copies Android splash screen files from dev directories into the appropriate platform specific location.
 | ||||
| // The code was extracted from here: http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/
 | ||||
| 
 | ||||
| var filesToCopy = [{ | ||||
|         'resources/android/splash/drawable-land-hdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-hdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-land-ldpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-ldpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-land-mdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-mdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-land-xhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-land-xxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-land-xxxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-port-hdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-hdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-port-ldpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-ldpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-port-mdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-mdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-port-xhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-port-xxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png' | ||||
|     }, { | ||||
|         'resources/android/splash/drawable-port-xxxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png' | ||||
|     } | ||||
| ]; | ||||
| 
 | ||||
| var fs = require('fs'); | ||||
| var path = require('path'); | ||||
| 
 | ||||
| // no need to configure below
 | ||||
| var rootDir = process.argv[2]; | ||||
| 
 | ||||
| filesToCopy.forEach(function(obj) { | ||||
|     Object.keys(obj).forEach(function(key) { | ||||
|         var val = obj[key]; | ||||
|         var srcFile = path.join(rootDir, key); | ||||
|         var destFile = path.join(rootDir, val); | ||||
|         var destDir = path.dirname(destFile); | ||||
|         if (fs.existsSync(srcFile) && fs.existsSync(destDir)) { | ||||
|             fs.createReadStream(srcFile).pipe(fs.createWriteStream(destFile)); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										18
									
								
								hooks/post_push
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								hooks/post_push
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| set -e | ||||
| 
 | ||||
| if [ "integration" != "${SOURCE_BRANCH}" ] | ||||
| then | ||||
|     # A space-separated list of additional tags to place on this image. | ||||
|     additionalTags=(latest) | ||||
| 
 | ||||
|     # Tag and push image for each additional tag | ||||
|     for tag in ${additionalTags[@]}; do | ||||
|         echo "Tagging {$IMAGE_NAME} as ${DOCKER_REPO}:${tag}" | ||||
|         docker tag $IMAGE_NAME ${DOCKER_REPO}:${tag} | ||||
| 
 | ||||
|         echo "Pushing ${DOCKER_REPO}:${tag}" | ||||
|         docker push ${DOCKER_REPO}:${tag} | ||||
|     done | ||||
| fi | ||||
| @ -6,6 +6,5 @@ | ||||
|   }, | ||||
|   "type": "ionic-angular", | ||||
|   "watchPatterns": [], | ||||
|   "pro_id": "com.moodle.moodlemobile", | ||||
|   "id": "com.moodle.moodlemobile" | ||||
| } | ||||
|  | ||||
							
								
								
									
										2611
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2611
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										26
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								package.json
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "moodlemobile", | ||||
|   "version": "3.6.0", | ||||
|   "version": "3.6.1", | ||||
|   "description": "The official app for Moodle.", | ||||
|   "author": { | ||||
|     "name": "Moodle Pty Ltd.", | ||||
| @ -35,7 +35,7 @@ | ||||
|     "preionic:build": "gulp", | ||||
|     "postionic:build": "gulp copy-component-templates", | ||||
|     "desktop.pack": "electron-builder --dir", | ||||
|     "desktop.dist": "electron-builder", | ||||
|     "desktop.dist": "electron-builder -p never", | ||||
|     "windows.store": "electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store --flatten true -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop" | ||||
|   }, | ||||
|   "dependencies": { | ||||
| @ -59,7 +59,7 @@ | ||||
|     "@ionic-native/globalization": "4.17.0", | ||||
|     "@ionic-native/in-app-browser": "4.17.0", | ||||
|     "@ionic-native/keyboard": "4.17.0", | ||||
|     "@ionic-native/local-notifications": "4.5.2", | ||||
|     "@ionic-native/local-notifications": "4.17.0", | ||||
|     "@ionic-native/media-capture": "4.17.0", | ||||
|     "@ionic-native/network": "4.17.0", | ||||
|     "@ionic-native/push": "4.17.0", | ||||
| @ -83,7 +83,6 @@ | ||||
|     "cordova-android-support-gradle-release": "2.0.1", | ||||
|     "cordova-clipboard": "1.2.1", | ||||
|     "cordova-ios": "4.5.5", | ||||
|     "cordova-plugin-app-event": "1.2.1", | ||||
|     "cordova-plugin-badge": "0.8.8", | ||||
|     "cordova-plugin-camera": "4.0.3", | ||||
|     "cordova-plugin-customurlscheme": "4.3.0", | ||||
| @ -94,7 +93,7 @@ | ||||
|     "cordova-plugin-globalization": "1.11.0", | ||||
|     "cordova-plugin-inappbrowser": "3.0.0", | ||||
|     "cordova-plugin-ionic-keyboard": "2.1.3", | ||||
|     "cordova-plugin-local-notifications-mm": "1.0.13", | ||||
|     "cordova-plugin-local-notification": "0.9.0-beta.3", | ||||
|     "cordova-plugin-media-capture": "3.0.2", | ||||
|     "cordova-plugin-network-information": "2.0.1", | ||||
|     "cordova-plugin-screen-orientation": "3.0.1", | ||||
| @ -103,14 +102,16 @@ | ||||
|     "cordova-plugin-whitelist": "1.3.3", | ||||
|     "cordova-plugin-zip": "3.1.0", | ||||
|     "cordova-sqlite-storage": "2.6.0", | ||||
|     "cordova-support-google-services": "1.2.1", | ||||
|     "es6-promise-plugin": "4.2.2", | ||||
|     "font-awesome": "4.7.0", | ||||
|     "ionic-angular": "3.9.2", | ||||
|     "ionic-angular": "3.9.3", | ||||
|     "ionicons": "3.0.0", | ||||
|     "jszip": "3.1.5", | ||||
|     "moment": "2.22.2", | ||||
|     "nl.kingsquare.cordova.background-audio": "1.0.1", | ||||
|     "phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle", | ||||
|     "phonegap-plugin-multidex": "1.0.0", | ||||
|     "phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v2", | ||||
|     "promise.prototype.finally": "3.1.0", | ||||
|     "rxjs": "5.5.11", | ||||
|     "sw-toolbox": "3.6.0", | ||||
| @ -119,9 +120,9 @@ | ||||
|     "zone.js": "0.8.26" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@ionic/app-scripts": "3.1.9", | ||||
|     "electron-rebuild": "1.8.1", | ||||
|     "@ionic/app-scripts": "3.2.2", | ||||
|     "electron-builder-lib": "20.23.1", | ||||
|     "electron-rebuild": "1.8.1", | ||||
|     "gulp": "4.0.0", | ||||
|     "gulp-clip-empty-files": "0.1.2", | ||||
|     "gulp-flatten": "0.4.0", | ||||
| @ -159,7 +160,7 @@ | ||||
|       "cordova-plugin-globalization": {}, | ||||
|       "cordova-plugin-inappbrowser": {}, | ||||
|       "cordova-plugin-ionic-keyboard": {}, | ||||
|       "cordova-plugin-local-notifications-mm": {}, | ||||
|       "cordova-plugin-local-notification": {}, | ||||
|       "cordova-plugin-media-capture": {}, | ||||
|       "cordova-plugin-network-information": {}, | ||||
|       "cordova-plugin-screen-orientation": {}, | ||||
| @ -170,7 +171,8 @@ | ||||
|       "cordova-sqlite-storage": {}, | ||||
|       "nl.kingsquare.cordova.background-audio": {}, | ||||
|       "phonegap-plugin-push": { | ||||
|         "SENDER_ID": "694767596569" | ||||
|         "ANDROID_SUPPORT_V13_VERSION": "27.+", | ||||
|         "FCM_VERSION": "17.0.+" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @ -206,7 +208,7 @@ | ||||
|       } | ||||
|     ], | ||||
|     "compression": "maximum", | ||||
|     "electronVersion": "2.0.4", | ||||
|     "electronVersion": "4.0.1", | ||||
|     "mac": { | ||||
|       "category": "public.app-category.education", | ||||
|       "icon": "resources/desktop/icon.icns", | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								resources/android/icon/drawable-hdpi-smallicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								resources/android/icon/drawable-hdpi-smallicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										
											BIN
										
									
								
								resources/android/icon/drawable-ldpi-smallicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								resources/android/icon/drawable-ldpi-smallicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										
											BIN
										
									
								
								resources/android/icon/drawable-mdpi-smallicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								resources/android/icon/drawable-mdpi-smallicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										
											BIN
										
									
								
								resources/android/icon/drawable-xhdpi-smallicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								resources/android/icon/drawable-xhdpi-smallicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
| @ -1,7 +1,7 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| # Compile AOT. | ||||
| if [ $TRAVIS_BRANCH == 'integration' ] || [ $TRAVIS_BRANCH == 'master' ] || [ -z $TRAVIS_BRANCH ] ; then | ||||
| if [ $TRAVIS_BRANCH == 'integration' ] || [ $TRAVIS_BRANCH == 'master' ] || [ $TRAVIS_BRANCH == 'desktop' ] || [ -z $TRAVIS_BRANCH ] ; then | ||||
|     cd scripts | ||||
|     ./build_lang.sh | ||||
|     cd .. | ||||
| @ -38,9 +38,8 @@ fi | ||||
| # Copy to PGB git (only on a configured travis build). | ||||
| if [ ! -z $GIT_ORG ] && [ ! -z $GIT_TOKEN ] ; then | ||||
|     gitfolder=${PWD##*/} | ||||
|     cd .. | ||||
|     git clone --depth 1 --no-single-branch https://github.com/$GIT_ORG/moodlemobile-phonegapbuild.git pgb | ||||
|     cd pgb | ||||
|     git clone --depth 1 --no-single-branch https://github.com/$GIT_ORG/moodlemobile-phonegapbuild.git ../pgb | ||||
|     pushd ../pgb | ||||
|     git checkout $TRAVIS_BRANCH | ||||
|     rm -Rf assets build index.html templates | ||||
|     cp -Rf ../$gitfolder/www/* ./ | ||||
| @ -48,4 +47,10 @@ if [ ! -z $GIT_ORG ] && [ ! -z $GIT_TOKEN ] ; then | ||||
|     git add . | ||||
|     git commit -m "Travis build: $TRAVIS_BUILD_NUMBER" | ||||
|     git push https://$GIT_TOKEN@github.com/$GIT_ORG/moodlemobile-phonegapbuild.git | ||||
|     popd | ||||
| fi | ||||
| 
 | ||||
| if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] && [ $TRAVIS_BRANCH == 'desktop' ] && [ $TRAVIS_OS_NAME == 'linux' ]; then | ||||
|     ./scripts/linux.sh | ||||
| fi | ||||
| 
 | ||||
|  | ||||
| @ -56,8 +56,19 @@ | ||||
|   "addon.block_timeline.pluginname": "block_timeline", | ||||
|   "addon.block_timeline.sortbycourses": "block_timeline", | ||||
|   "addon.block_timeline.sortbydates": "block_timeline", | ||||
|   "addon.blog.blog": "blog", | ||||
|   "addon.blog.blogentries": "blog", | ||||
|   "addon.blog.errorloadentries": "local_moodlemobileapp", | ||||
|   "addon.blog.linktooriginalentry": "blog", | ||||
|   "addon.blog.noentriesyet": "blog", | ||||
|   "addon.blog.publishtonoone": "blog", | ||||
|   "addon.blog.publishtosite": "blog", | ||||
|   "addon.blog.publishtoworld": "blog", | ||||
|   "addon.blog.showonlyyourentries": "local_moodlemobileapp", | ||||
|   "addon.blog.siteblogheading": "blog", | ||||
|   "addon.calendar.calendar": "calendar", | ||||
|   "addon.calendar.calendarevents": "local_moodlemobileapp", | ||||
|   "addon.calendar.calendarreminders": "local_moodlemobileapp", | ||||
|   "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", | ||||
|   "addon.calendar.errorloadevent": "local_moodlemobileapp", | ||||
|   "addon.calendar.errorloadevents": "local_moodlemobileapp", | ||||
| @ -65,7 +76,8 @@ | ||||
|   "addon.calendar.eventstarttime": "calendar", | ||||
|   "addon.calendar.gotoactivity": "calendar", | ||||
|   "addon.calendar.noevents": "local_moodlemobileapp", | ||||
|   "addon.calendar.notifications": "local_moodlemobileapp", | ||||
|   "addon.calendar.reminders": "local_moodlemobileapp", | ||||
|   "addon.calendar.setnewreminder": "local_moodlemobileapp", | ||||
|   "addon.calendar.typecategory": "calendar", | ||||
|   "addon.calendar.typeclose": "calendar", | ||||
|   "addon.calendar.typecourse": "calendar", | ||||
| @ -133,6 +145,7 @@ | ||||
|   "addon.coursecompletion.criteriarequiredany": "completion", | ||||
|   "addon.coursecompletion.inprogress": "completion", | ||||
|   "addon.coursecompletion.manualselfcompletion": "completion", | ||||
|   "addon.coursecompletion.nottracked": "completion", | ||||
|   "addon.coursecompletion.notyetstarted": "completion", | ||||
|   "addon.coursecompletion.pending": "completion", | ||||
|   "addon.coursecompletion.required": "moodle", | ||||
| @ -214,6 +227,9 @@ | ||||
|   "addon.messages.unabletomessage": "message", | ||||
|   "addon.messages.unblockuser": "message", | ||||
|   "addon.messages.unblockuserconfirm": "message", | ||||
|   "addon.messages.useentertosend": "message", | ||||
|   "addon.messages.useentertosenddescdesktop": "local_moodlemobileapp", | ||||
|   "addon.messages.useentertosenddescmac": "local_moodlemobileapp", | ||||
|   "addon.messages.userwouldliketocontactyou": "message", | ||||
|   "addon.messages.warningconversationmessagenotsent": "local_moodlemobileapp", | ||||
|   "addon.messages.warningmessagenotsent": "local_moodlemobileapp", | ||||
| @ -328,9 +344,12 @@ | ||||
|   "addon.mod_assign_submission_comments.pluginname": "assignsubmission_comments", | ||||
|   "addon.mod_assign_submission_file.pluginname": "assignsubmission_file", | ||||
|   "addon.mod_assign_submission_onlinetext.pluginname": "assignsubmission_onlinetext", | ||||
|   "addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext", | ||||
|   "addon.mod_book.errorchapter": "book", | ||||
|   "addon.mod_book.modulenameplural": "book", | ||||
|   "addon.mod_book.toc": "book", | ||||
|   "addon.mod_chat.beep": "chat", | ||||
|   "addon.mod_chat.chatreport": "chat", | ||||
|   "addon.mod_chat.currentusers": "chat", | ||||
|   "addon.mod_chat.enterchat": "chat", | ||||
|   "addon.mod_chat.entermessage": "chat", | ||||
| @ -342,12 +361,16 @@ | ||||
|   "addon.mod_chat.messagebeepsyou": "chat", | ||||
|   "addon.mod_chat.messageenter": "chat", | ||||
|   "addon.mod_chat.messageexit": "chat", | ||||
|   "addon.mod_chat.messages": "chat", | ||||
|   "addon.mod_chat.modulenameplural": "chat", | ||||
|   "addon.mod_chat.mustbeonlinetosendmessages": "local_moodlemobileapp", | ||||
|   "addon.mod_chat.nomessages": "chat", | ||||
|   "addon.mod_chat.nosessionsfound": "local_moodlemobileapp", | ||||
|   "addon.mod_chat.send": "chat", | ||||
|   "addon.mod_chat.sessionstart": "chat", | ||||
|   "addon.mod_chat.showincompletesessions": "local_moodlemobileapp", | ||||
|   "addon.mod_chat.talk": "chat", | ||||
|   "addon.mod_chat.viewreport": "chat", | ||||
|   "addon.mod_choice.cannotsubmit": "choice", | ||||
|   "addon.mod_choice.choiceoptions": "choice", | ||||
|   "addon.mod_choice.errorgetchoice": "local_moodlemobileapp", | ||||
| @ -602,6 +625,7 @@ | ||||
|   "addon.mod_lti.modulenameplural": "lti", | ||||
|   "addon.mod_page.errorwhileloadingthepage": "local_moodlemobileapp", | ||||
|   "addon.mod_page.modulenameplural": "page", | ||||
|   "addon.mod_quiz.answercolon": "qtype_numerical", | ||||
|   "addon.mod_quiz.attemptfirst": "quiz", | ||||
|   "addon.mod_quiz.attemptlast": "quiz", | ||||
|   "addon.mod_quiz.attemptnumber": "quiz", | ||||
| @ -732,6 +756,7 @@ | ||||
|   "addon.mod_scorm.scormstatusnotdownloaded": "local_moodlemobileapp", | ||||
|   "addon.mod_scorm.scormstatusoutdated": "local_moodlemobileapp", | ||||
|   "addon.mod_scorm.suspended": "scorm", | ||||
|   "addon.mod_scorm.toc": "scorm", | ||||
|   "addon.mod_scorm.warningofflinedatadeleted": "local_moodlemobileapp", | ||||
|   "addon.mod_scorm.warningsynconlineincomplete": "local_moodlemobileapp", | ||||
|   "addon.mod_survey.cannotsubmitsurvey": "local_moodlemobileapp", | ||||
| @ -1246,6 +1271,7 @@ | ||||
|   "core.courses.enrolme": "local_moodlemobileapp", | ||||
|   "core.courses.errorloadcategories": "local_moodlemobileapp", | ||||
|   "core.courses.errorloadcourses": "local_moodlemobileapp", | ||||
|   "core.courses.errorloadplugins": "local_moodlemobileapp", | ||||
|   "core.courses.errorsearching": "local_moodlemobileapp", | ||||
|   "core.courses.errorselfenrol": "local_moodlemobileapp", | ||||
|   "core.courses.filtermycourses": "local_moodlemobileapp", | ||||
| @ -1278,6 +1304,7 @@ | ||||
|   "core.defaultvalue": "tool_usertours", | ||||
|   "core.delete": "moodle", | ||||
|   "core.deletedoffline": "local_moodlemobileapp", | ||||
|   "core.deleteduser": "bulkusers", | ||||
|   "core.deleting": "local_moodlemobileapp", | ||||
|   "core.description": "moodle", | ||||
|   "core.dfdaymonthyear": "local_moodlemobileapp", | ||||
| @ -1484,6 +1511,7 @@ | ||||
|   "core.maxsizeandattachments": "moodle", | ||||
|   "core.min": "moodle", | ||||
|   "core.mins": "moodle", | ||||
|   "core.misc": "admin", | ||||
|   "core.mod_assign": "assign/pluginname", | ||||
|   "core.mod_assignment": "assignment/pluginname", | ||||
|   "core.mod_book": "book/pluginname", | ||||
| @ -1528,6 +1556,7 @@ | ||||
|   "core.noresults": "moodle", | ||||
|   "core.notapplicable": "local_moodlemobileapp", | ||||
|   "core.notice": "moodle", | ||||
|   "core.notingroup": "moodle", | ||||
|   "core.notsent": "local_moodlemobileapp", | ||||
|   "core.now": "moodle", | ||||
|   "core.numwords": "moodle", | ||||
| @ -1567,11 +1596,20 @@ | ||||
|   "core.question.questionno": "question", | ||||
|   "core.question.requiresgrading": "question", | ||||
|   "core.quotausage": "moodle", | ||||
|   "core.rating.aggregateavg": "moodle", | ||||
|   "core.rating.aggregatecount": "moodle", | ||||
|   "core.rating.aggregatemax": "moodle", | ||||
|   "core.rating.aggregatemin": "moodle", | ||||
|   "core.rating.aggregatesum": "moodle", | ||||
|   "core.rating.noratings": "moodle", | ||||
|   "core.rating.rating": "moodle", | ||||
|   "core.rating.ratings": "moodle", | ||||
|   "core.redirectingtosite": "local_moodlemobileapp", | ||||
|   "core.refresh": "moodle", | ||||
|   "core.remove": "moodle", | ||||
|   "core.required": "moodle", | ||||
|   "core.requireduserdatamissing": "local_moodlemobileapp", | ||||
|   "core.resourcedisplayopen": "moodle", | ||||
|   "core.resources": "moodle", | ||||
|   "core.restore": "moodle", | ||||
|   "core.retry": "local_moodlemobileapp", | ||||
| @ -1626,6 +1664,7 @@ | ||||
|   "core.settings.navigatoruseragent": "local_moodlemobileapp", | ||||
|   "core.settings.networkstatus": "local_moodlemobileapp", | ||||
|   "core.settings.privacypolicy": "local_moodlemobileapp", | ||||
|   "core.settings.pushid": "local_moodlemobileapp", | ||||
|   "core.settings.reportinbackground": "local_moodlemobileapp", | ||||
|   "core.settings.settings": "moodle", | ||||
|   "core.settings.showdownloadoptions": "local_moodlemobileapp", | ||||
|  | ||||
							
								
								
									
										34
									
								
								scripts/linux.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										34
									
								
								scripts/linux.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,34 @@ | ||||
| #!/bin/bash | ||||
| # | ||||
| # Script for generating the Desktop builds | ||||
| # | ||||
| 
 | ||||
| sudo apt-get install -y libnss3-dev | ||||
| 
 | ||||
| npm install -g electron-builder electron | ||||
| 
 | ||||
| electron-builder install-app-deps | ||||
| 
 | ||||
| jq -s '.[0] + {"name": "moodledesktop"}' package.json > package_new.json | ||||
| mv package_new.json package.json | ||||
| 
 | ||||
| rm -Rf desktop/dist | ||||
| 
 | ||||
| npm run desktop.dist -- -l --x64 --ia32 | ||||
| 
 | ||||
| if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] ; then | ||||
|     git clone -q https://$GIT_TOKEN@github.com/moodlemobile/bma-apps-builds.git ../apps | ||||
| 
 | ||||
|     mv desktop/dist/*.AppImage ../apps | ||||
| 
 | ||||
|     cd ../apps | ||||
| 
 | ||||
|     chmod +x *.AppImage | ||||
|     mv *i386.AppImage linux-ia32.AppImage | ||||
|     mv Moodle*.AppImage linux-x64.AppImage | ||||
|     ls | ||||
| 
 | ||||
|     git add . | ||||
|     git commit -m "Linux desktop versions from Travis build $TRAVIS_BUILD_NUMBER" | ||||
|     git push | ||||
| fi | ||||
| @ -19,7 +19,7 @@ import { CoreLoginHelperProvider } from '@core/login/providers/helper'; | ||||
| import { AddonBadgesProvider } from './badges'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to user participants page. | ||||
|  * Handler to treat links to user badges page. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonBadgesMyBadgesLinkHandler extends CoreContentLinksHandlerBase { | ||||
|  | ||||
							
								
								
									
										51
									
								
								src/addon/blog/blog.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/addon/blog/blog.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; | ||||
| import { CoreUserDelegate } from '@core/user/providers/user-delegate'; | ||||
| import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; | ||||
| import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; | ||||
| import { AddonBlogProvider } from './providers/blog'; | ||||
| import { AddonBlogMainMenuHandler } from './providers/mainmenu-handler'; | ||||
| import { AddonBlogUserHandler } from './providers/user-handler'; | ||||
| import { AddonBlogCourseOptionHandler } from './providers/course-option-handler'; | ||||
| import { AddonBlogComponentsModule } from './components/components.module'; | ||||
| import { AddonBlogIndexLinkHandler } from './providers/index-link-handler'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|     ], | ||||
|     imports: [ | ||||
|         AddonBlogComponentsModule | ||||
|     ], | ||||
|     providers: [ | ||||
|         AddonBlogProvider, | ||||
|         AddonBlogMainMenuHandler, | ||||
|         AddonBlogUserHandler, | ||||
|         AddonBlogCourseOptionHandler, | ||||
|         AddonBlogIndexLinkHandler | ||||
|     ] | ||||
| }) | ||||
| export class AddonBlogModule { | ||||
|     constructor(mainMenuDelegate: CoreMainMenuDelegate, menuHandler: AddonBlogMainMenuHandler, | ||||
|             userHandler: AddonBlogUserHandler, userDelegate: CoreUserDelegate, | ||||
|             courseOptionHandler: AddonBlogCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate, | ||||
|             linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate) { | ||||
|         mainMenuDelegate.registerHandler(menuHandler); | ||||
|         userDelegate.registerHandler(userHandler); | ||||
|         courseOptionsDelegate.registerHandler(courseOptionHandler); | ||||
|         contentLinksDelegate.registerHandler(linkHandler); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										47
									
								
								src/addon/blog/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/addon/blog/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { IonicModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CorePipesModule } from '@pipes/pipes.module'; | ||||
| import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; | ||||
| import { AddonBlogEntriesComponent } from './entries/entries'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonBlogEntriesComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         TranslateModule.forChild(), | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CorePipesModule, | ||||
|         CoreCommentsComponentsModule | ||||
|     ], | ||||
|     providers: [ | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonBlogEntriesComponent | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonBlogEntriesComponent | ||||
|     ] | ||||
| }) | ||||
| export class AddonBlogComponentsModule {} | ||||
							
								
								
									
										49
									
								
								src/addon/blog/components/entries/addon-blog-entries.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/addon/blog/components/entries/addon-blog-entries.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| <ion-content> | ||||
|     <ion-refresher [enabled]="loaded" (ionRefresh)="refresh($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
|         <div class="safe-padding-horizontal"> | ||||
|             <ion-item *ngIf="showMyIssuesToggle"> | ||||
|                 <ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label> | ||||
|                 <ion-toggle [(ngModel)]="onlyMyEntries"></ion-toggle> | ||||
|             </ion-item> | ||||
|         </div> | ||||
|         <core-empty-box *ngIf="entries && entries.length == 0" icon="fa-newspaper-o" [message]="'addon.blog.noentriesyet' | translate"></core-empty-box> | ||||
|         <ng-container *ngFor="let entry of entries"> | ||||
|             <ion-card *ngIf="!onlyMyEntries || entry.userid == currentUserId"> | ||||
|                 <ion-item text-wrap> | ||||
|                     <ion-avatar core-user-avatar [user]="entry.user" item-start [courseId]="entry.courseid"></ion-avatar> | ||||
|                     <h2> | ||||
|                         <core-format-text [text]="entry.subject"></core-format-text> | ||||
|                         <ion-note float-end padding-left text-end> | ||||
|                             {{ 'addon.blog.' + entry.publishTranslated | translate}} | ||||
|                         </ion-note> | ||||
|                     </h2> | ||||
|                     <p> | ||||
|                         <ion-note float-end padding-left text-end> | ||||
|                             {{entry.created | coreDateDayOrTime}} | ||||
|                         </ion-note> | ||||
|                         {{entry.user && entry.user.fullname}} | ||||
|                     </p> | ||||
|                 </ion-item> | ||||
|                 <ion-card-content> | ||||
|                     <core-format-text [text]="entry.summary" [component]="this.component" [componentId]="entry.id"></core-format-text> | ||||
|                     <ion-item> | ||||
|                         <core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid" contextLevel="user"></core-comments> | ||||
|                     </ion-item> | ||||
|                     <core-file *ngFor="let file of entry.attachmentfiles" [file]="file" [component]="this.component" [componentId]="entry.id"></core-file> | ||||
|                     <a ion-item *ngIf="entry.uniquehash" [href]="entry.uniquehash" core-link>{{ 'addon.blog.linktooriginalentry' | translate }}</a> | ||||
|                 </ion-card-content> | ||||
|                 <ion-row text-center> | ||||
|                     <ion-col *ngIf="entry.lastmodified > entry.created"> | ||||
|                         <ion-note> | ||||
|                             <ion-icon name="time"></ion-icon> {{entry.lastmodified | coreTimeAgo}} | ||||
|                         </ion-note> | ||||
|                     </ion-col> | ||||
|                 </ion-row> | ||||
|             </ion-card> | ||||
|         </ng-container> | ||||
|         <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										176
									
								
								src/addon/blog/components/entries/entries.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/addon/blog/components/entries/entries.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,176 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input, OnInit, ViewChild } from '@angular/core'; | ||||
| import { Content } from 'ionic-angular'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { AddonBlogProvider } from '../../providers/blog'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays the blog entries. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-blog-entries', | ||||
|     templateUrl: 'addon-blog-entries.html', | ||||
| }) | ||||
| export class AddonBlogEntriesComponent implements OnInit { | ||||
|     @Input() userId?: number; | ||||
|     @Input() courseId?: number; | ||||
|     @Input() cmId?: number; | ||||
|     @Input() entryId?: number; | ||||
|     @Input() groupId?: number; | ||||
|     @Input() tagId?: number; | ||||
| 
 | ||||
|     protected filter = {}; | ||||
|     protected pageLoaded = 0; | ||||
| 
 | ||||
|     @ViewChild(Content) content: Content; | ||||
| 
 | ||||
|     loaded = false; | ||||
|     canLoadMore = false; | ||||
|     loadMoreError = false; | ||||
|     entries = []; | ||||
|     currentUserId: number; | ||||
|     showMyIssuesToggle = false; | ||||
|     onlyMyEntries = false; | ||||
|     component = AddonBlogProvider.COMPONENT; | ||||
| 
 | ||||
|     constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider, | ||||
|             protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider) { | ||||
|         this.currentUserId = sitesProvider.getCurrentSiteUserId(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         if (this.userId) { | ||||
|             this.filter['userid'] = this.userId; | ||||
|         } | ||||
| 
 | ||||
|         if (this.courseId) { | ||||
|             this.filter['courseid'] = this.courseId; | ||||
|         } | ||||
| 
 | ||||
|         if (this.cmId) { | ||||
|             this.filter['cmid'] = this.cmId; | ||||
|         } | ||||
| 
 | ||||
|         if (this.entryId) { | ||||
|             this.filter['entryid'] = this.entryId; | ||||
|         } | ||||
| 
 | ||||
|         if (this.groupId) { | ||||
|             this.filter['groupid'] = this.groupId; | ||||
|         } | ||||
| 
 | ||||
|         if (this.tagId) { | ||||
|             this.filter['tagid'] = this.tagId; | ||||
|         } | ||||
| 
 | ||||
|         this.fetchEntries().then(() => { | ||||
|             this.blogProvider.logView(this.filter).catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch blog entries. | ||||
|      * | ||||
|      * @param {boolean} [refresh] Empty events array first. | ||||
|      * @return {Promise<any>}         Promise with the entries. | ||||
|      */ | ||||
|     private fetchEntries(refresh: boolean = false): Promise<any> { | ||||
|         this.loadMoreError = false; | ||||
| 
 | ||||
|         if (refresh) { | ||||
|             this.pageLoaded = 0; | ||||
|         } | ||||
| 
 | ||||
|         return this.blogProvider.getEntries(this.filter, this.pageLoaded).then((result) => { | ||||
|             const promises = result.entries.map((entry) => { | ||||
|                 switch (entry.publishstate) { | ||||
|                     case 'draft': | ||||
|                         entry.publishTranslated = 'publishtonoone'; | ||||
|                         break; | ||||
|                     case 'site': | ||||
|                         entry.publishTranslated = 'publishtosite'; | ||||
|                         break; | ||||
|                     case 'public': | ||||
|                         entry.publishTranslated = 'publishtoworld'; | ||||
|                         break; | ||||
|                     default: | ||||
|                         entry.publishTranslated = 'privacy:unknown'; | ||||
|                         break; | ||||
|                 } | ||||
| 
 | ||||
|                 return this.userProvider.getProfile(entry.userid, entry.courseid, true).then((user) => { | ||||
|                     entry.user = user; | ||||
|                 }).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }); | ||||
|             }); | ||||
| 
 | ||||
|             if (refresh) { | ||||
|                 this.showMyIssuesToggle = false; | ||||
|                 this.entries = result.entries; | ||||
|             } else { | ||||
|                 this.entries = this.entries.concat(result.entries); | ||||
|             } | ||||
| 
 | ||||
|             this.canLoadMore = result.totalentries > this.entries.length; | ||||
|             this.pageLoaded++; | ||||
| 
 | ||||
|             this.showMyIssuesToggle = !this.userId; | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }).catch((message) => { | ||||
|             this.domUtils.showErrorModalDefault(message, 'addon.blog.errorloadentries', true); | ||||
|             this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
 | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function to load more entries. | ||||
|      * | ||||
|      * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     loadMore(infiniteComplete?: any): Promise<any> { | ||||
|         return this.fetchEntries().finally(() => { | ||||
|             infiniteComplete && infiniteComplete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh blog entries on PTR. | ||||
|      * | ||||
|      * @param {any}     refresher  Refresher instance. | ||||
|      */ | ||||
|     refresh(refresher?: any): void { | ||||
|         this.blogProvider.invalidateEntries(this.filter).finally(() => { | ||||
|             this.fetchEntries(true).finally(() => { | ||||
|                 if (refresher) { | ||||
|                     refresher.complete(); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										12
									
								
								src/addon/blog/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/addon/blog/lang/en.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| { | ||||
|     "blog": "Blog", | ||||
|     "blogentries": "Blog entries", | ||||
|     "errorloadentries": "Error loading blog entries.", | ||||
|     "linktooriginalentry": "Link to original blog entry", | ||||
|     "noentriesyet": "No visible entries here", | ||||
|     "publishtonoone": "Yourself (draft)", | ||||
|     "publishtosite": "Anyone on this site", | ||||
|     "publishtoworld": "Anyone in the world", | ||||
|     "showonlyyourentries": "Show only your entries", | ||||
|     "siteblogheading": "Site blog" | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/addon/blog/pages/entries/entries.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/addon/blog/pages/entries/entries.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title>{{ title | translate }}</ion-title> | ||||
|         <ion-buttons end></ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <addon-blog-entries class="core-avoid-header" [courseId]="courseId" [userId]="userId" [cmId]="cmId" [entryId]="entryId" [groupId]="groupId" [tagId]="tagId"></addon-blog-entries> | ||||
							
								
								
									
										33
									
								
								src/addon/blog/pages/entries/entries.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/addon/blog/pages/entries/entries.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { IonicPageModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { AddonBlogEntriesPage } from './entries'; | ||||
| import { AddonBlogComponentsModule } from '../../components/components.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonBlogEntriesPage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreDirectivesModule, | ||||
|         AddonBlogComponentsModule, | ||||
|         IonicPageModule.forChild(AddonBlogEntriesPage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class AddonBlogEntriesPageModule {} | ||||
							
								
								
									
										49
									
								
								src/addon/blog/pages/entries/entries.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/addon/blog/pages/entries/entries.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component } from '@angular/core'; | ||||
| import { IonicPage, NavParams } from 'ionic-angular'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the list of blog entries. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-blog-entries' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-blog-entries', | ||||
|     templateUrl: 'entries.html', | ||||
| }) | ||||
| export class AddonBlogEntriesPage { | ||||
|     userId: number; | ||||
|     courseId: number; | ||||
|     cmId: number; | ||||
|     entryId: number; | ||||
|     groupId: number; | ||||
|     tagId: number; | ||||
|     title: string; | ||||
| 
 | ||||
|     constructor(params: NavParams) { | ||||
|         this.userId = params.get('userId'); | ||||
|         this.courseId = params.get('courseId'); | ||||
|         this.cmId = params.get('cmId'); | ||||
|         this.entryId = params.get('entryId'); | ||||
|         this.groupId = params.get('groupId'); | ||||
|         this.tagId = params.get('tagId'); | ||||
| 
 | ||||
|         if (!this.userId && !this.courseId && !this.cmId && !this.entryId && !this.groupId && !this.tagId) { | ||||
|             this.title = 'addon.blog.siteblogheading'; | ||||
|         } else { | ||||
|             this.title = 'addon.blog.blogentries'; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										113
									
								
								src/addon/blog/providers/blog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/addon/blog/providers/blog.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,113 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle blog entries. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonBlogProvider { | ||||
|     static ENTRIES_PER_PAGE = 10; | ||||
|     static COMPONENT = 'blog'; | ||||
|     protected ROOT_CACHE_KEY = 'addonBlog:'; | ||||
|     protected logger; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected utils: CoreUtilsProvider) { | ||||
|         this.logger = logger.getInstance('AddonBlogProvider'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not the blog plugin is enabled for a certain site. | ||||
|      * | ||||
|      * This method is called quite often and thus should only perform a quick | ||||
|      * check, we should not be calling WS from here. | ||||
|      * | ||||
|      * @param  {string} [siteId]  Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false or rejected otherwise. | ||||
|      */ | ||||
|     isPluginEnabled(siteId?: string): Promise<boolean> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.wsAvailable('core_blog_get_entries') && | ||||
|                 site.canUseAdvancedFeature('enableblogs'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the cache key for the blog entries. | ||||
|      * | ||||
|      * @param  {any}     [filter]     Filter to apply on search. | ||||
|      * @return {string}          Cache key. | ||||
|      */ | ||||
|     getEntriesCacheKey(filter: any = {}): string { | ||||
|         return this.ROOT_CACHE_KEY + this.utils.sortAndStringify(filter); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get blog entries. | ||||
|      * | ||||
|      * @param  {any}     [filter]     Filter to apply on search. | ||||
|      * @param  {any}     [page=0]     Page of the blog entries to fetch. | ||||
|      * @param  {string}  [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}         Promise to be resolved when the entries are retrieved. | ||||
|      */ | ||||
|     getEntries(filter: any = {}, page: number = 0, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const data = { | ||||
|                 filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value'), | ||||
|                 page: page, | ||||
|                 perpage: AddonBlogProvider.ENTRIES_PER_PAGE | ||||
|             }; | ||||
| 
 | ||||
|             const preSets = { | ||||
|                 cacheKey: this.getEntriesCacheKey(filter) | ||||
|             }; | ||||
| 
 | ||||
|             return site.read('core_blog_get_entries', data, preSets); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate blog entries WS call. | ||||
|      * | ||||
|      * @param  {any}     [filter]     Filter to apply on search | ||||
|      * @param  {string}  [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}         Promise resolved when data is invalidated. | ||||
|      */ | ||||
|     invalidateEntries(filter: any = {}, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.invalidateWsCacheForKey(this.getEntriesCacheKey(filter)); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Trigger the blog_entries_viewed event. | ||||
|      * | ||||
|      * @param  {any}     [filter]     Filter to apply on search. | ||||
|      * @param  {string}  [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}         Promise to be resolved when done. | ||||
|      */ | ||||
|     logView(filter: any = {}, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const data = { | ||||
|                 filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value') | ||||
|             }; | ||||
| 
 | ||||
|             return site.write('core_blog_view_entries', data); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										120
									
								
								src/addon/blog/providers/course-option-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/addon/blog/providers/course-option-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable, Injector } from '@angular/core'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | ||||
| import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; | ||||
| import { CoreCoursesProvider } from '@core/courses/providers/courses'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseHelperProvider } from '@core/course/providers/helper'; | ||||
| import { AddonBlogEntriesComponent } from '../components/entries/entries'; | ||||
| import { AddonBlogProvider } from './blog'; | ||||
| 
 | ||||
| /** | ||||
|  * Course nav handler. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonBlogCourseOptionHandler implements CoreCourseOptionsHandler { | ||||
|     name = 'AddonBlog'; | ||||
|     priority = 100; | ||||
| 
 | ||||
|     constructor(protected coursesProvider: CoreCoursesProvider, protected blogProvider: AddonBlogProvider, | ||||
|         protected courseHelper: CoreCourseHelperProvider, protected courseProvider: CoreCourseProvider, | ||||
|         protected sitesProvider: CoreSitesProvider, protected filepoolProvider: CoreFilepoolProvider) {} | ||||
| 
 | ||||
|     /** | ||||
|      * Should invalidate the data to determine if the handler is enabled for a certain course. | ||||
|      * | ||||
|      * @param {number} courseId The course ID. | ||||
|      * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise<any> { | ||||
|         return this.courseProvider.invalidateCourseBlocks(courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return {boolean} Whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return this.blogProvider.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled for a certain course. | ||||
|      * | ||||
|      * @param {number} courseId The course ID. | ||||
|      * @param {any} accessData Access type and data. Default, guest, ... | ||||
|      * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled. | ||||
|      */ | ||||
|     isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> { | ||||
|         return this.courseHelper.hasABlockNamed(courseId, 'blog_menu').then((enabled) => { | ||||
|             if (enabled && navOptions && typeof navOptions.blogs != 'undefined') { | ||||
|                 return navOptions.blogs; | ||||
|             } | ||||
| 
 | ||||
|             return enabled; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the data needed to render the handler. | ||||
|      * | ||||
|      * @param {Injector} injector Injector. | ||||
|      * @param {number} courseId The course ID. | ||||
|      * @return {CoreCourseOptionsHandlerData|Promise<CoreCourseOptionsHandlerData>} Data or promise resolved with the data. | ||||
|      */ | ||||
|     getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> { | ||||
|         return { | ||||
|             title: 'addon.blog.blog', | ||||
|             class: 'addon-blog-handler', | ||||
|             component: AddonBlogEntriesComponent | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. | ||||
|      * | ||||
|      * @param {any} course The course. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     prefetch(course: any): Promise<any> { | ||||
|         const siteId = this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         return this.blogProvider.getEntries({courseid: course.id}).then((result) => { | ||||
|             return result.entries.map((entry) => { | ||||
|                 let files = []; | ||||
| 
 | ||||
|                 if (entry.attachmentfiles && entry.attachmentfiles.length) { | ||||
|                     files = entry.attachmentfiles; | ||||
|                 } | ||||
|                 if (entry.summaryfiles && entry.summaryfiles.length) { | ||||
|                     files = files.concat(entry.summaryfiles); | ||||
|                 } | ||||
| 
 | ||||
|                 if (files.length > 0) { | ||||
|                     return this.filepoolProvider.addFilesToQueue(siteId, files, entry.module, entry.id); | ||||
|                 } | ||||
| 
 | ||||
|                 return Promise.resolve(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										76
									
								
								src/addon/blog/providers/index-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/addon/blog/providers/index-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; | ||||
| import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | ||||
| import { CoreLoginHelperProvider } from '@core/login/providers/helper'; | ||||
| import { AddonBlogProvider } from './blog'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to blog page. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonBlogIndexLinkHandler extends CoreContentLinksHandlerBase { | ||||
|     name = 'AddonBlogIndexLinkHandler'; | ||||
|     featureName = 'CoreUserDelegate_AddonBlog'; | ||||
|     pattern = /\/blog\/index\.php/; | ||||
| 
 | ||||
|     constructor(private blogProvider: AddonBlogProvider, private loginHelper: CoreLoginHelperProvider) { | ||||
|         super(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the list of actions for a link (url). | ||||
|      * | ||||
|      * @param {string[]} siteIds List of sites the URL belongs to. | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} [courseId] Course ID related to the URL. Optional but recommended. | ||||
|      * @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions. | ||||
|      */ | ||||
|     getActions(siteIds: string[], url: string, params: any, courseId?: number): | ||||
|             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { | ||||
|         const pageParams: any = {}; | ||||
| 
 | ||||
|         params.userid ? pageParams['userId'] = parseInt(params.userid, 10) : null; | ||||
|         params.modid ? pageParams['cmId'] = parseInt(params.modid, 10) : null; | ||||
|         params.courseid ? pageParams['courseId'] = parseInt(params.courseid, 10) : null; | ||||
|         params.entryid ? pageParams['entryId'] = parseInt(params.entryid, 10) : null; | ||||
|         params.groupid ? pageParams['groupId'] = parseInt(params.groupid, 10) : null; | ||||
|         params.tagid ? pageParams['tagId'] = parseInt(params.tagid, 10) : null; | ||||
| 
 | ||||
|         return [{ | ||||
|             action: (siteId, navCtrl?): void => { | ||||
|                 // Always use redirect to make it the new history root (to avoid "loops" in history).
 | ||||
|                 this.loginHelper.redirect('AddonBlogEntriesPage', pageParams, siteId); | ||||
|             } | ||||
|         }]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled for a certain site (site + user) and a URL. | ||||
|      * If not defined, defaults to true. | ||||
|      * | ||||
|      * @param {string} siteId The site ID. | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} [courseId] Course ID related to the URL. Optional but recommended. | ||||
|      * @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site. | ||||
|      */ | ||||
|     isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> { | ||||
| 
 | ||||
|         return this.blogProvider.isPluginEnabled(siteId); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										51
									
								
								src/addon/blog/providers/mainmenu-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/addon/blog/providers/mainmenu-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { AddonBlogProvider } from './blog'; | ||||
| import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to inject an option into main menu. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonBlogMainMenuHandler implements CoreMainMenuHandler { | ||||
|     name = 'AddonBlog'; | ||||
|     priority = 450; | ||||
| 
 | ||||
|     constructor(private blogProvider: AddonBlogProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return {boolean} Whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return this.blogProvider.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the data needed to render the handler. | ||||
|      * | ||||
|      * @return {CoreMainMenuHandlerData} Data needed to render the handler. | ||||
|      */ | ||||
|     getDisplayData(): CoreMainMenuHandlerData { | ||||
|         return { | ||||
|             icon: 'fa-newspaper-o', | ||||
|             title: 'addon.blog.siteblogheading', | ||||
|             page: 'AddonBlogEntriesPage', | ||||
|             class: 'addon-blog-handler' | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										71
									
								
								src/addon/blog/providers/user-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/addon/blog/providers/user-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; | ||||
| import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; | ||||
| import { AddonBlogProvider } from './blog'; | ||||
| 
 | ||||
| /** | ||||
|  * Profile item handler. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonBlogUserHandler implements CoreUserProfileHandler { | ||||
|     name = 'AddonBlog:blogs'; | ||||
|     priority = 300; | ||||
|     type = CoreUserDelegate.TYPE_NEW_PAGE; | ||||
| 
 | ||||
|     constructor(protected linkHelper: CoreContentLinksHelperProvider, protected blogProvider: AddonBlogProvider) { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return this.blogProvider.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if handler is enabled for this user in this context. | ||||
|      * | ||||
|      * @param {any} user User to check. | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @return {boolean|Promise<boolean>} Promise resolved with true if enabled, resolved with false otherwise. | ||||
|      */ | ||||
|     isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the data needed to render the handler. | ||||
|      * | ||||
|      * @return {CoreUserProfileHandlerData} Data needed to render the handler. | ||||
|      */ | ||||
|     getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { | ||||
|         return { | ||||
|             icon: 'fa-newspaper-o', | ||||
|             title: 'addon.blog.blogentries', | ||||
|             class: 'addon-blog-handler', | ||||
|             action: (event, navCtrl, user, courseId): void => { | ||||
|                 event.preventDefault(); | ||||
|                 event.stopPropagation(); | ||||
|                 // Always use redirect to make it the new history root (to avoid "loops" in history).
 | ||||
|                 this.linkHelper.goInSite(navCtrl, 'AddonBlogEntriesPage', { userId: user.id, courseId: courseId }); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @ -71,7 +71,7 @@ export class AddonCalendarModule { | ||||
|             newName: AddonCalendarProvider.EVENTS_TABLE, | ||||
|             filterFields: ['id', 'name', 'description', 'format', 'eventtype', 'courseid', 'timestart', 'timeduration', | ||||
|                     'categoryid', 'groupid', 'userid', 'instance', 'modulename', 'timemodified', 'repeatid', 'visible', 'uuid', | ||||
|                     'sequence', 'subscriptionid', 'notificationtime'] | ||||
|                     'sequence', 'subscriptionid'] | ||||
|         }); | ||||
| 
 | ||||
|         // Migrate the component name.
 | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| { | ||||
|     "calendar": "Calendar", | ||||
|     "calendarevents": "Calendar events", | ||||
|     "calendarreminders": "Calendar reminders", | ||||
|     "defaultnotificationtime": "Default notification time", | ||||
|     "errorloadevent": "Error loading event.", | ||||
|     "errorloadevents": "Error loading events.", | ||||
| @ -8,7 +9,8 @@ | ||||
|     "eventstarttime": "Start time", | ||||
|     "gotoactivity": "Go to activity", | ||||
|     "noevents": "There are no events", | ||||
|     "notifications": "Notifications", | ||||
|     "reminders": "Reminders", | ||||
|     "setnewreminder": "Set a new reminder", | ||||
|     "typeclose": "Close event", | ||||
|     "typecourse": "Course event", | ||||
|     "typecategory": "Category event", | ||||
|  | ||||
| @ -10,10 +10,10 @@ | ||||
|     <core-loading [hideUntil]="eventLoaded"> | ||||
|         <ion-card> | ||||
|             <ion-card-content *ngIf="event"> | ||||
|                 <ion-card-title text-wrap> | ||||
|                 <ion-item text-wrap> | ||||
|                     <core-icon *ngIf="event.icon && !event.moduleIcon" [name]="event.icon" item-start></core-icon> | ||||
|                     <core-format-text [text]="event.name"></core-format-text> | ||||
|                 </ion-card-title> | ||||
|                     <h2><core-format-text [text]="event.name"></core-format-text></h2> | ||||
|                 </ion-item> | ||||
|                 <ion-item text-wrap> | ||||
|                     <h2>{{ 'addon.calendar.eventstarttime' | translate}}</h2> | ||||
|                     <p>{{ event.timestart * 1000 | coreFormatDate }}</p> | ||||
| @ -52,21 +52,29 @@ | ||||
|             </ion-card-content> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <ion-card list *ngIf="notificationsEnabled && event.timestart - 600 > currentTime"> | ||||
|         <ion-card list *ngIf="notificationsEnabled"> | ||||
|             <ion-item> | ||||
|                 <ion-label>{{ 'addon.calendar.notifications' | translate }}</ion-label> | ||||
|                 <ion-select [(ngModel)]="notificationTime" (ionChange)="updateNotificationTime($event)" interface="action-sheet"> | ||||
|                     <ion-option value="-1" *ngIf="event.timestart - defaultTime > currentTime">{{ 'core.defaultvalue' | translate :{$a: defaultTimeReadable} }}</ion-option> | ||||
|                     <ion-option value="0">{{ 'core.settings.disabled' | translate }}</ion-option> | ||||
|                     <ion-option value="10">{{ 600 | coreDuration }}</ion-option> | ||||
|                     <ion-option value="30" *ngIf="event.timestart - 1800 > currentTime">{{ 1800 | coreDuration }}</ion-option> | ||||
|                     <ion-option value="60" *ngIf="event.timestart - 3600 > currentTime">{{ 3600 | coreDuration }}</ion-option> | ||||
|                     <ion-option value="120" *ngIf="event.timestart - 7200 > currentTime">{{ 7200 | coreDuration }}</ion-option> | ||||
|                     <ion-option value="360" *ngIf="event.timestart - 21600 > currentTime">{{ 21600 | coreDuration }}</ion-option> | ||||
|                     <ion-option value="720" *ngIf="event.timestart - 43200 > currentTime">{{ 43200 | coreDuration }}</ion-option> | ||||
|                     <ion-option value="1440" *ngIf="event.timestart - 86400 > currentTime">{{ 86400 | coreDuration }}</ion-option> | ||||
|                 </ion-select> | ||||
|                 <h2>{{ 'addon.calendar.reminders' | translate }}</h2> | ||||
|             </ion-item> | ||||
|             <ng-container *ngFor="let reminder of reminders"> | ||||
|                 <ion-item  *ngIf="reminder.time > 0 || defaultTime > 0" [class.item-dimmed]="(reminder.time == -1 ? (event.timestart - defaultTime) : reminder.time) <= currentTime" > | ||||
|                     <p *ngIf="reminder.time == -1">{{ 'core.defaultvalue' | translate :{$a: ((event.timestart - defaultTime) * 1000) | coreFormatDate } }}</p> | ||||
|                     <p *ngIf="reminder.time > 0">{{ reminder.time * 1000 | coreFormatDate }}</p> | ||||
|                     <button ion-button icon-only clear="true" (click)="cancelNotification(reminder.id, $event)" [attr.aria-label]=" 'core.delete' | translate" item-end *ngIf="(reminder.time == -1 ? (event.timestart - defaultTime) : reminder.time) > currentTime"> | ||||
|                         <ion-icon name="trash" color="danger"></ion-icon> | ||||
|                     </button> | ||||
|                 </ion-item> | ||||
|             </ng-container> | ||||
| 
 | ||||
|             <ng-container *ngIf="event.timestart + event.timeduration > currentTime"> | ||||
|                 <ion-item> | ||||
|                     <ion-label stacked>{{ 'addon.calendar.setnewreminder' | translate }}</ion-label> | ||||
|                     <ion-datetime [(ngModel)]="notificationTimeText" [placeholder]="'core.choosedots' | translate" [displayFormat]="notificationFormat" [min]="notificationMin" [max]="notificationMax"></ion-datetime> | ||||
|                 </ion-item> | ||||
|                 <ion-item> | ||||
|                     <button ion-button block color="primary" (click)="addNotificationTime($event)" [disabled]="!notificationTimeText">{{ 'addon.calendar.setnewreminder' | translate }}</button> | ||||
|                 </ion-item> | ||||
|             </ng-container> | ||||
|         </ion-card> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
|  | ||||
| @ -39,8 +39,10 @@ export class AddonCalendarEventPage { | ||||
|     protected eventId; | ||||
|     protected siteHomeId: number; | ||||
|     eventLoaded: boolean; | ||||
|     notificationTime: number; | ||||
|     defaultTimeReadable: string; | ||||
|     notificationFormat: string; | ||||
|     notificationMin: string; | ||||
|     notificationMax: string; | ||||
|     notificationTimeText: string; | ||||
|     event: any = {}; | ||||
|     title: string; | ||||
|     courseName: string; | ||||
| @ -50,6 +52,7 @@ export class AddonCalendarEventPage { | ||||
|     categoryPath = ''; | ||||
|     currentTime: number; | ||||
|     defaultTime: number; | ||||
|     reminders: any[]; | ||||
| 
 | ||||
|     constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, | ||||
|             private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, | ||||
| @ -61,21 +64,17 @@ export class AddonCalendarEventPage { | ||||
|         this.notificationsEnabled = localNotificationsProvider.isAvailable(); | ||||
|         this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); | ||||
|         if (this.notificationsEnabled) { | ||||
|             this.calendarProvider.getEventNotificationTimeOption(this.eventId).then((notificationTime) => { | ||||
|                 this.notificationTime = notificationTime; | ||||
|                 this.loadNotificationTime(); | ||||
|             this.calendarProvider.getEventReminders(this.eventId).then((reminders) => { | ||||
|                 this.reminders = reminders; | ||||
|             }); | ||||
| 
 | ||||
|             this.calendarProvider.getDefaultNotificationTime().then((defaultTime) => { | ||||
|                 this.defaultTime = defaultTime * 60; | ||||
|                 this.loadNotificationTime(); | ||||
|                 if (defaultTime === 0) { | ||||
|                     // Disabled by default.
 | ||||
|                     this.defaultTimeReadable = this.translate.instant('core.settings.disabled'); | ||||
|                 } else { | ||||
|                     this.defaultTimeReadable = timeUtils.formatTime(defaultTime * 60); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|         // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
 | ||||
|         this.notificationFormat = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatetimeshort')) | ||||
|             .replace(/[\[\]]/g, ''); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -88,12 +87,6 @@ export class AddonCalendarEventPage { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     updateNotificationTime(): void { | ||||
|         if (!isNaN(this.notificationTime) && this.event && this.event.id) { | ||||
|             this.calendarProvider.updateNotificationTime(this.event, this.notificationTime); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches the event and updates the view. | ||||
|      * | ||||
| @ -117,7 +110,9 @@ export class AddonCalendarEventPage { | ||||
|             this.event = event; | ||||
| 
 | ||||
|             this.currentTime = this.timeUtils.timestamp(); | ||||
|             this.loadNotificationTime(); | ||||
|             this.notificationMin = this.timeUtils.userDate(this.currentTime * 1000, 'YYYY-MM-DDTHH:mm', false); | ||||
|             this.notificationMax = this.timeUtils.userDate((event.timestart + event.timeduration) * 1000, | ||||
|                 'YYYY-MM-DDTHH:mm', false); | ||||
| 
 | ||||
|             // Reset some of the calculated data.
 | ||||
|             this.categoryPath = ''; | ||||
| @ -187,18 +182,52 @@ export class AddonCalendarEventPage { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads notification time by discarding options not in the list. | ||||
|      * Add a reminder for this event. | ||||
|      * | ||||
|      * @param {Event} e    Click event. | ||||
|      */ | ||||
|     loadNotificationTime(): void { | ||||
|         if (typeof this.notificationTime != 'undefined') { | ||||
|             if (this.notificationTime > 0 && this.event.timestart - this.notificationTime * 60 < this.currentTime) { | ||||
|                 this.notificationTime = 0; | ||||
|             } else if (this.notificationTime < 0 && this.event.timestart - this.defaultTime < this.currentTime) { | ||||
|                 this.notificationTime = 0; | ||||
|     addNotificationTime(e: Event): void { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         if (this.notificationTimeText && this.event && this.event.id) { | ||||
|             let notificationTime = this.timeUtils.convertToTimestamp(this.notificationTimeText); | ||||
| 
 | ||||
|             const currentTime = this.timeUtils.timestamp(), | ||||
|                 minute = Math.floor(currentTime / 60) * 60; | ||||
| 
 | ||||
|             // Check if the notification time is in the same minute as we are, so the notification is triggered.
 | ||||
|             if (notificationTime >=  minute && notificationTime < minute + 60) { | ||||
|                 notificationTime  = currentTime + 1; | ||||
|             } | ||||
| 
 | ||||
|             this.calendarProvider.addEventReminder(this.event, notificationTime).then(() => { | ||||
|                 this.calendarProvider.getEventReminders(this.eventId).then((reminders) => { | ||||
|                     this.reminders = reminders; | ||||
|                 }); | ||||
| 
 | ||||
|                 this.notificationTimeText = null; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Cancel the selected notification. | ||||
|      * | ||||
|      * @param {number} id  Reminder ID. | ||||
|      * @param {Event} e    Click event. | ||||
|      */ | ||||
|     cancelNotification(id: number, e: Event): void { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
|         this.calendarProvider.deleteEventReminder(id).then(() => { | ||||
|             this.calendarProvider.getEventReminders(this.eventId).then((reminders) => { | ||||
|                 this.reminders = reminders; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the event. | ||||
|      * | ||||
|  | ||||
| @ -13,9 +13,8 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import { CoreCoursesProvider } from '@core/courses/providers/courses'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| @ -23,6 +22,8 @@ import { CoreGroupsProvider } from '@providers/groups'; | ||||
| import { CoreConstants } from '@core/constants'; | ||||
| import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; | ||||
| import { CoreConfigProvider } from '@providers/config'; | ||||
| import { ILocalNotification } from '@ionic-native/local-notifications'; | ||||
| import { SQLiteDB } from '@classes/sqlitedb'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle calendar events. | ||||
| @ -37,134 +38,219 @@ export class AddonCalendarProvider { | ||||
|     protected ROOT_CACHE_KEY = 'mmaCalendar:'; | ||||
| 
 | ||||
|     // Variables for database.
 | ||||
|     static EVENTS_TABLE = 'addon_calendar_events'; | ||||
|     protected tablesSchema = [ | ||||
|         { | ||||
|             name: AddonCalendarProvider.EVENTS_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'id', | ||||
|                     type: 'INTEGER', | ||||
|                     primaryKey: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'notificationtime', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'name', | ||||
|                     type: 'TEXT', | ||||
|                     notNull: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'description', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'format', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'eventtype', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timestart', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timeduration', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'categoryid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'groupid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'userid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'instance', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'modulename', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'repeatid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'visible', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'uuid', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'sequence', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'subscriptionid', | ||||
|                     type: 'INTEGER' | ||||
|                 } | ||||
|             ] | ||||
|     static EVENTS_TABLE = 'addon_calendar_events_2'; | ||||
|     static REMINDERS_TABLE = 'addon_calendar_reminders'; | ||||
|     protected siteSchema: CoreSiteSchema = { | ||||
|         name: 'AddonCalendarProvider', | ||||
|         version: 2, | ||||
|         tables: [ | ||||
|             { | ||||
|                 name: AddonCalendarProvider.EVENTS_TABLE, | ||||
|                 columns: [ | ||||
|                     { | ||||
|                         name: 'id', | ||||
|                         type: 'INTEGER', | ||||
|                         primaryKey: true | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'name', | ||||
|                         type: 'TEXT', | ||||
|                         notNull: true | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'description', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'format', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'eventtype', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'courseid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'timestart', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'timeduration', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'categoryid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'groupid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'userid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'instance', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'modulename', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'timemodified', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'repeatid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'visible', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'uuid', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'sequence', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'subscriptionid', | ||||
|                         type: 'INTEGER' | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 name: AddonCalendarProvider.REMINDERS_TABLE, | ||||
|                 columns: [ | ||||
|                     { | ||||
|                         name: 'id', | ||||
|                         type: 'INTEGER', | ||||
|                         primaryKey: true | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'eventid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'time', | ||||
|                         type: 'INTEGER' | ||||
|                     } | ||||
|                 ], | ||||
|                 uniqueKeys: [ | ||||
|                     ['eventid', 'time'] | ||||
|                 ] | ||||
|             } | ||||
|         ], | ||||
|         migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise<any> | void { | ||||
|             if (oldVersion < 2) { | ||||
|                 const newTable = AddonCalendarProvider.EVENTS_TABLE; | ||||
|                 const oldTable = 'addon_calendar_events'; | ||||
| 
 | ||||
|                 return db.tableExists(oldTable).then(() => { | ||||
|                     return db.getAllRecords(oldTable).then((events) => { | ||||
|                         const now = Math.round(Date.now() / 1000); | ||||
| 
 | ||||
|                         return Promise.all(events.map((event) => { | ||||
|                             if (event.notificationtime == 0) { | ||||
|                                 // No reminders.
 | ||||
|                                 return Promise.resolve(); | ||||
|                             } | ||||
| 
 | ||||
|                             let time; | ||||
| 
 | ||||
|                             if (event.notificationtime == -1) { | ||||
|                                 time = -1; | ||||
|                             } else { | ||||
|                                 time = event.timestart - event.notificationtime * 60; | ||||
| 
 | ||||
|                                 if (time < now) { | ||||
|                                     // Old reminder, just not add this.
 | ||||
|                                     return Promise.resolve(); | ||||
|                                 } | ||||
|                             } | ||||
| 
 | ||||
|                             const reminder = { | ||||
|                                 eventid: event.id, | ||||
|                                 time: time | ||||
|                             }; | ||||
| 
 | ||||
|                             // Cancel old notification.
 | ||||
|                             this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, siteId); | ||||
| 
 | ||||
|                             return db.insertRecord(AddonCalendarProvider.REMINDERS_TABLE, reminder); | ||||
|                         })).then(() => { | ||||
|                             // Move the records from the old table.
 | ||||
|                             return db.insertRecordsFrom(newTable, oldTable, undefined, 'id, name, description, format, eventtype,\ | ||||
|                                 courseid, timestart, timeduration, categoryid, groupid, userid, instance, modulename, timemodified,\ | ||||
|                                 repeatid, visible, uuid, sequence, subscriptionid'); | ||||
|                         }).then(() => { | ||||
|                             return db.dropTable(oldTable); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }).catch(() => { | ||||
|                     // Old table does not exist, ignore.
 | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     ]; | ||||
|     }; | ||||
| 
 | ||||
|     protected logger; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, | ||||
|             private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, | ||||
|             private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, | ||||
|             private translate: TranslateService) { | ||||
|             private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider) { | ||||
|         this.logger = logger.getInstance('AddonCalendarProvider'); | ||||
|         this.sitesProvider.createTablesFromSchema(this.tablesSchema); | ||||
|         this.sitesProvider.registerSiteSchema(this.siteSchema); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes expired events from local DB. | ||||
|      * | ||||
|      * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. | ||||
|      * @return {Promise<void>} Promise resolved when done. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     cleanExpiredEvents(siteId?: string): Promise<void> { | ||||
|     cleanExpiredEvents(siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             let promise; | ||||
|             return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?', | ||||
|                     [this.timeUtils.timestamp()]).then((events) => { | ||||
|                 return Promise.all(events.map((event) => { | ||||
|                     return this.deleteEvent(event.id, siteId); | ||||
|                 })); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|             // Cancel expired events notifications first.
 | ||||
|             if (this.localNotificationsProvider.isAvailable()) { | ||||
|                 promise = site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart < ?', | ||||
|                         [this.timeUtils.timestamp()]).then((events) => { | ||||
|                     events.forEach((event) => { | ||||
|                         return this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, site.getId()); | ||||
|                     }); | ||||
|                 }).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }); | ||||
|             } else { | ||||
|                 promise = Promise.resolve(); | ||||
|             } | ||||
|     /** | ||||
|      * Delete event cancelling all the reminders and notifications. | ||||
|      * | ||||
|      * @param  {number}       eventId Event ID. | ||||
|      * @param  {string}       [siteId] ID of the site the event belongs to. If not defined, use current site. | ||||
|      * @return {Promise<any>}         Resolved when done. | ||||
|      */ | ||||
|     protected deleteEvent(eventId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             siteId = site.getId(); | ||||
| 
 | ||||
|             return promise.then(() => { | ||||
|                 return site.getDb().deleteRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart < ?', | ||||
|                     [this.timeUtils.timestamp()]); | ||||
|             const promises = []; | ||||
| 
 | ||||
|             promises.push(site.getDb().deleteRecords(AddonCalendarProvider.EVENTS_TABLE, {id: eventId})); | ||||
| 
 | ||||
|             promises.push(site.getDb().getRecords(AddonCalendarProvider.REMINDERS_TABLE, {eventid: eventId}).then((reminders) => { | ||||
|                 return Promise.all(reminders.map((reminder) => { | ||||
|                     return this.deleteEventReminder(reminder.id, siteId); | ||||
|                 })); | ||||
|             })); | ||||
| 
 | ||||
|             return Promise.all(promises).catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| @ -279,36 +365,53 @@ export class AddonCalendarProvider { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get event notification time. Always returns number of minutes (0 if disabled). | ||||
|      * Adds an event reminder and schedule a new notification. | ||||
|      * | ||||
|      * @param  {number} id       Event ID. | ||||
|      * @param  {any} event       Event to update its notification time. | ||||
|      * @param  {number} time     New notification setting timestamp. | ||||
|      * @param  {string} [siteId] ID of the site the event belongs to. If not defined, use current site. | ||||
|      * @return {Promise<number>}  Event notification time in minutes. 0 if disabled. | ||||
|      * @return {Promise<any>} Promise resolved when the notification is updated. | ||||
|      */ | ||||
|     getEventNotificationTime(id: number, siteId?: string): Promise<number> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
|     addEventReminder(event: any, time: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const reminder = { | ||||
|                 eventid: event.id, | ||||
|                 time: time | ||||
|             }; | ||||
| 
 | ||||
|         return this.getEventNotificationTimeOption(id, siteId).then((time: number) => { | ||||
|             if (time == -1) { | ||||
|                 return this.getDefaultNotificationTime(siteId); | ||||
|             } | ||||
| 
 | ||||
|             return time; | ||||
|             return site.getDb().insertRecord(AddonCalendarProvider.REMINDERS_TABLE, reminder).then((reminderId) => { | ||||
|                 return this.scheduleEventNotification(event, reminderId, time, site.getId()); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get event notification time for options. Returns -1 for default time. | ||||
|      * Remove an event reminder and cancel the notification. | ||||
|      * | ||||
|      * @param  {number} id       Reminder ID. | ||||
|      * @param  {string} [siteId] ID of the site the event belongs to. If not defined, use current site. | ||||
|      * @return {Promise<any>} Promise resolved when the notification is updated. | ||||
|      */ | ||||
|     deleteEventReminder(id: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             if (this.localNotificationsProvider.isAvailable()) { | ||||
|                 this.localNotificationsProvider.cancel(id, AddonCalendarProvider.COMPONENT, site.getId()); | ||||
|             } | ||||
| 
 | ||||
|             return site.getDb().deleteRecords(AddonCalendarProvider.REMINDERS_TABLE, {id: id}); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a calendar reminders from local Db. | ||||
|      * | ||||
|      * @param  {number} id       Event ID. | ||||
|      * @param  {string} [siteId] ID of the site the event belongs to. If not defined, use current site. | ||||
|      * @return {Promise<number>}  Promise with wvent notification time in minutes. 0 if disabled, -1 if default time. | ||||
|      * @return {Promise<any>}    Promise resolved when the event data is retrieved. | ||||
|      */ | ||||
|     getEventNotificationTimeOption(id: number, siteId?: string): Promise<number> { | ||||
|         return this.getEventFromLocalDb(id, siteId).then((e) => { | ||||
|             return e.notificationtime || -1; | ||||
|         }).catch(() => { | ||||
|             return -1; | ||||
|     getEventReminders(id: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().getRecords(AddonCalendarProvider.REMINDERS_TABLE, {eventid: id}, 'time ASC'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -507,38 +610,48 @@ export class AddonCalendarProvider { | ||||
|      * @param  {string} [siteId] Site ID the event belongs to. If not defined, use current site. | ||||
|      * @return {Promise<void>}    Promise resolved when the notification is scheduled. | ||||
|      */ | ||||
|     scheduleEventNotification(event: any, time: number, siteId?: string): Promise<void> { | ||||
|     protected scheduleEventNotification(event: any, reminderId: number, time: number, siteId?: string): Promise<void> { | ||||
|         if (this.localNotificationsProvider.isAvailable()) { | ||||
|             siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|             if (time === 0) { | ||||
|                 // Cancel if it was scheduled.
 | ||||
|                 return this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, siteId); | ||||
|                 return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); | ||||
|             } | ||||
| 
 | ||||
|             // If time is -1, get event default time.
 | ||||
|             const promise = time == -1 ? this.getDefaultNotificationTime(siteId) : Promise.resolve(time); | ||||
|             let promise; | ||||
|             if (time == -1) { | ||||
|                 // If time is -1, get event default time to calculate the notification time.
 | ||||
|                 promise = this.getDefaultNotificationTime(siteId).then((time) => { | ||||
|                     if (time == 0) { | ||||
|                         // Default notification time is disabled, do not show.
 | ||||
|                         return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); | ||||
|                     } | ||||
| 
 | ||||
|                     return event.timestart - (time * 60); | ||||
|                 }); | ||||
|             } else { | ||||
|                 promise = Promise.resolve(time); | ||||
|             } | ||||
| 
 | ||||
|             return promise.then((time) => { | ||||
|                 const timeEnd = (event.timestart + event.timeduration) * 1000; | ||||
|                 if (timeEnd <= new Date().getTime()) { | ||||
|                     // The event has finished already, don't schedule it.
 | ||||
|                     return Promise.resolve(); | ||||
|                 time = time * 1000; | ||||
| 
 | ||||
|                 if (time <= new Date().getTime()) { | ||||
|                     // This reminder is over, don't schedule. Cancel if it was scheduled.
 | ||||
|                     return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); | ||||
|                 } | ||||
| 
 | ||||
|                 const dateTriggered = new Date((event.timestart - (time * 60)) * 1000), | ||||
|                     notification = { | ||||
|                         id: event.id, | ||||
|                 const notification: ILocalNotification = { | ||||
|                         id: reminderId, | ||||
|                         title: event.name, | ||||
|                         text: this.timeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true), | ||||
|                         at: dateTriggered, | ||||
|                         channelParams: { | ||||
|                             channelID: 'notifications', | ||||
|                             channelName: this.translate.instant('addon.notifications.notifications'), | ||||
|                             importance: 4 // IMPORTANCE_HIGH
 | ||||
|                         trigger: { | ||||
|                             at: new Date(time) | ||||
|                         }, | ||||
|                         data: { | ||||
|                             eventid: event.id, | ||||
|                             reminderid: reminderId, | ||||
|                             siteid: siteId | ||||
|                         } | ||||
|                     }; | ||||
| @ -561,18 +674,27 @@ export class AddonCalendarProvider { | ||||
|      * @return {Promise<any[]>}         Promise resolved when all the notifications have been scheduled. | ||||
|      */ | ||||
|     scheduleEventsNotifications(events: any[], siteId?: string): Promise<any[]> { | ||||
|         const promises = []; | ||||
| 
 | ||||
|         if (this.localNotificationsProvider.isAvailable()) { | ||||
|             siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
|             events.forEach((e) => { | ||||
|                 promises.push(this.getEventNotificationTime(e.id, siteId).then((time) => { | ||||
|                     return this.scheduleEventNotification(e, time, siteId); | ||||
|                 })); | ||||
|             }); | ||||
| 
 | ||||
|             return Promise.all(events.map((event) => { | ||||
|                 const timeEnd = (event.timestart + event.timeduration) * 1000; | ||||
| 
 | ||||
|                 if (timeEnd <= new Date().getTime()) { | ||||
|                     // The event has finished already, don't schedule it.
 | ||||
|                     return this.deleteEvent(event.id, siteId); | ||||
|                 } | ||||
| 
 | ||||
|                 return this.getEventReminders(event.id, siteId).then((reminders) => { | ||||
|                     return Promise.all(reminders.map((reminder) => { | ||||
|                         return this.scheduleEventNotification(event, reminder.id, reminder.time, siteId); | ||||
|                     })); | ||||
|                 }); | ||||
|             })); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.all(promises); | ||||
|         return Promise.resolve([]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -599,7 +721,41 @@ export class AddonCalendarProvider { | ||||
|      */ | ||||
|     storeEventInLocalDb(event: any, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, event); | ||||
|             siteId = site.getId(); | ||||
| 
 | ||||
|             // If event does not exist on the DB, schedule the reminder.
 | ||||
|             return this.getEventFromLocalDb(event.id, site.id).catch(() => { | ||||
|                 // Event does not exist. Check if any reminder exists first.
 | ||||
|                 return this.getEventReminders(event.id, siteId).then((reminders) => { | ||||
|                     if (reminders.length == 0) { | ||||
|                         this.addEventReminder(event, -1, siteId); | ||||
|                     } | ||||
|                 }); | ||||
|             }).then(() => { | ||||
|                 const eventRecord = { | ||||
|                     id: event.id, | ||||
|                     name: event.name, | ||||
|                     description: event.description, | ||||
|                     format: event.format, | ||||
|                     eventtype: event.eventtype, | ||||
|                     courseid: event.courseid, | ||||
|                     timestart: event.timestart, | ||||
|                     timeduration: event.timeduration, | ||||
|                     categoryid: event.categoryid, | ||||
|                     groupid: event.groupid, | ||||
|                     userid: event.userid, | ||||
|                     instance: event.instance, | ||||
|                     modulename: event.modulename, | ||||
|                     timemodified: event.timemodified, | ||||
|                     repeatid: event.repeatid, | ||||
|                     visible: event.visible, | ||||
|                     uuid: event.uuid, | ||||
|                     sequence: event.sequence, | ||||
|                     subscriptionid: event.subscriptionid | ||||
|                 }; | ||||
| 
 | ||||
|                 return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, eventRecord); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -614,65 +770,10 @@ export class AddonCalendarProvider { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             siteId = site.getId(); | ||||
| 
 | ||||
|             const promises = [], | ||||
|                 db = site.getDb(); | ||||
| 
 | ||||
|             events.forEach((event) => { | ||||
|                 // Don't override event notification time if the user configured it.
 | ||||
|                 promises.push(this.getEventFromLocalDb(event.id, siteId).catch(() => { | ||||
|                     // Event not stored, return empty object.
 | ||||
|                     return {}; | ||||
|                 }).then((e) => { | ||||
|                     const eventRecord = { | ||||
|                         id: event.id, | ||||
|                         name: event.name, | ||||
|                         description: event.description, | ||||
|                         format: event.format, | ||||
|                         eventtype: event.eventtype, | ||||
|                         courseid: event.courseid, | ||||
|                         timestart: event.timestart, | ||||
|                         timeduration: event.timeduration, | ||||
|                         categoryid: event.categoryid, | ||||
|                         groupid: event.groupid, | ||||
|                         userid: event.userid, | ||||
|                         instance: event.instance, | ||||
|                         modulename: event.modulename, | ||||
|                         timemodified: event.timemodified, | ||||
|                         repeatid: event.repeatid, | ||||
|                         visible: event.visible, | ||||
|                         uuid: event.uuid, | ||||
|                         sequence: event.sequence, | ||||
|                         subscriptionid: event.subscriptionid, | ||||
|                         notificationtime: e.notificationtime || -1 | ||||
|                     }; | ||||
| 
 | ||||
|                     return db.insertRecord(AddonCalendarProvider.EVENTS_TABLE, eventRecord); | ||||
|                 })); | ||||
|             }); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates an event notification time and schedule a new notification. | ||||
|      * | ||||
|      * @param  {any} event Event to update its notification time. | ||||
|      * @param  {number} time  New notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start". | ||||
|      * @param  {string} [siteId] ID of the site the event belongs to. If not defined, use current site. | ||||
|      * @return {Promise<void>} Promise resolved when the notification is updated. | ||||
|      */ | ||||
|     updateNotificationTime(event: any, time: number, siteId?: string): Promise<void> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             if (!this.sitesProvider.isLoggedIn()) { | ||||
|                 // Not logged in, we can't get the site DB. User logged out or session expired while an operation was ongoing.
 | ||||
|                 return Promise.reject(null); | ||||
|             } | ||||
| 
 | ||||
|             return site.getDb().updateRecords(AddonCalendarProvider.EVENTS_TABLE, {notificationtime: time}, {id: event.id}) | ||||
|                     .then(() => { | ||||
|                 return this.scheduleEventNotification(event, time); | ||||
|             }); | ||||
|             return Promise.all(events.map((event) => { | ||||
|                 // If event does not exist on the DB, schedule the reminder.
 | ||||
|                 return this.storeEventInLocalDb(event, siteId); | ||||
|             })); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="completionLoaded"> | ||||
|         <ion-card *ngIf="completion"> | ||||
|         <ion-card *ngIf="completion && tracked"> | ||||
|             <ion-item text-wrap> | ||||
|                 <h2>{{ 'addon.coursecompletion.status' | translate }}</h2> | ||||
|                 <p>{{ completion.statusText | translate }}</p> | ||||
| @ -14,7 +14,7 @@ | ||||
|                 <p *ngIf="completion.aggregation === 2">{{ 'addon.coursecompletion.criteriarequiredany' | translate }}</p> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
|         <ion-card *ngIf="completion"> | ||||
|         <ion-card *ngIf="completion && tracked"> | ||||
|             <ion-item-divider>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</ion-item-divider> | ||||
|             <ion-item class="hidden-tablet" text-wrap *ngFor="let criteria of completion.completions"> | ||||
|                 <h2><core-format-text clean="true" [text]="criteria.details.criteria"></core-format-text></h2> | ||||
| @ -41,11 +41,16 @@ | ||||
|                 </ion-row> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
|         <ion-card *ngIf="showSelfComplete"> | ||||
|         <ion-card *ngIf="showSelfComplete && tracked"> | ||||
|             <ion-item-divider>{{ 'addon.coursecompletion.manualselfcompletion' | translate }}</ion-item-divider> | ||||
|             <ion-item> | ||||
|                 <button ion-button block (click)="completeCourse()">{{ 'addon.coursecompletion.completecourse' | translate }}</button> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <div *ngIf="!tracked" class="core-warning-card" icon-start> | ||||
|             <ion-icon name="warning"></ion-icon> | ||||
|             {{ 'addon.coursecompletion.nottracked' | translate }} | ||||
|         </div> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
|  | ||||
| @ -31,6 +31,7 @@ export class AddonCourseCompletionReportComponent implements OnInit { | ||||
|     completionLoaded = false; | ||||
|     completion: any; | ||||
|     showSelfComplete: boolean; | ||||
|     tracked = true; // Whether completion is tracked.
 | ||||
| 
 | ||||
|     constructor( | ||||
|         private sitesProvider: CoreSitesProvider, | ||||
| @ -62,8 +63,14 @@ export class AddonCourseCompletionReportComponent implements OnInit { | ||||
| 
 | ||||
|             this.completion = completion; | ||||
|             this.showSelfComplete = this.courseCompletionProvider.canMarkSelfCompleted(this.userId, completion); | ||||
|         }).catch((message) => { | ||||
|             this.domUtils.showErrorModalDefault(message, 'addon.coursecompletion.couldnotloadreport', true); | ||||
|             this.tracked = true; | ||||
|         }).catch((error) => { | ||||
|             if (error && error.errorcode == 'notenroled') { | ||||
|                 // Not enrolled error, probably a teacher.
 | ||||
|                 this.tracked = false; | ||||
|             } else { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'addon.coursecompletion.couldnotloadreport', true); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -12,6 +12,7 @@ | ||||
|     "criteriarequiredany": "Any criteria below are required", | ||||
|     "inprogress": "In progress", | ||||
|     "manualselfcompletion": "Manual self completion", | ||||
|     "nottracked": "You are currently not being tracked by completion in this course", | ||||
|     "notyetstarted": "Not yet started", | ||||
|     "pending": "Pending", | ||||
|     "required": "Required", | ||||
|  | ||||
| @ -62,7 +62,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy { | ||||
| 
 | ||||
|         // Update discussions when new message is received.
 | ||||
|         this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => { | ||||
|             if (data.userId) { | ||||
|             if (data.userId && this.discussions) { | ||||
|                 const discussion = this.discussions.find((disc) => { | ||||
|                     return disc.message.user == data.userId; | ||||
|                 }); | ||||
| @ -82,7 +82,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy { | ||||
| 
 | ||||
|         // Update discussions when a message is read.
 | ||||
|         this.readChangedObserver = eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => { | ||||
|             if (data.userId) { | ||||
|             if (data.userId && this.discussions) { | ||||
|                 const discussion = this.discussions.find((disc) => { | ||||
|                     return disc.message.user == data.userId; | ||||
|                 }); | ||||
| @ -92,8 +92,8 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy { | ||||
|                     discussion.unread = false; | ||||
| 
 | ||||
|                     // Conversations changed, invalidate them and refresh unread counts.
 | ||||
|                     this.messagesProvider.invalidateConversations(); | ||||
|                     this.messagesProvider.refreshUnreadConversationCounts(); | ||||
|                     this.messagesProvider.invalidateConversations(this.siteId); | ||||
|                     this.messagesProvider.refreshUnreadConversationCounts(this.siteId); | ||||
|                 } | ||||
|             } | ||||
|         }, this.siteId); | ||||
| @ -145,10 +145,10 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy { | ||||
|      */ | ||||
|     refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise<any> { | ||||
|         const promises = []; | ||||
|         promises.push(this.messagesProvider.invalidateDiscussionsCache()); | ||||
|         promises.push(this.messagesProvider.invalidateDiscussionsCache(this.siteId)); | ||||
| 
 | ||||
|         if (refreshUnreadCounts) { | ||||
|             promises.push(this.messagesProvider.invalidateUnreadConversationCounts()); | ||||
|             promises.push(this.messagesProvider.invalidateUnreadConversationCounts(this.siteId)); | ||||
|         } | ||||
| 
 | ||||
|         return this.utils.allPromises(promises).finally(() => { | ||||
| @ -171,7 +171,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy { | ||||
| 
 | ||||
|         const promises = []; | ||||
| 
 | ||||
|         promises.push(this.messagesProvider.getDiscussions().then((discussions) => { | ||||
|         promises.push(this.messagesProvider.getDiscussions(this.siteId).then((discussions) => { | ||||
|             // Convert to an array for sorting.
 | ||||
|             const discussionsSorted = []; | ||||
|             for (const userId in discussions) { | ||||
| @ -184,7 +184,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy { | ||||
|             }); | ||||
|         })); | ||||
| 
 | ||||
|         promises.push(this.messagesProvider.getUnreadConversationCounts()); | ||||
|         promises.push(this.messagesProvider.getUnreadConversationCounts(this.siteId)); | ||||
| 
 | ||||
|         return Promise.all(promises).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); | ||||
| @ -216,7 +216,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy { | ||||
|         this.loaded = false; | ||||
|         this.loadingMessage = this.search.loading; | ||||
| 
 | ||||
|         return this.messagesProvider.searchMessages(query).then((searchResults) => { | ||||
|         return this.messagesProvider.searchMessages(query, undefined, undefined, undefined, this.siteId).then((searchResults) => { | ||||
|             this.search.showResults = true; | ||||
|             this.search.results = searchResults.messages; | ||||
|         }).catch((error) => { | ||||
|  | ||||
| @ -66,11 +66,14 @@ | ||||
|     "unabletomessage": "You are unable to message this user", | ||||
|     "unblockuser": "Unblock user", | ||||
|     "unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", | ||||
|     "useentertosend": "Use enter to send", | ||||
|     "useentertosenddescdesktop": "If disabled, you can use Ctrl+Enter to send the message.", | ||||
|     "useentertosenddescmac": "If disabled, you can use Cmd+Enter to send the message.", | ||||
|     "userwouldliketocontactyou": "{{$a}} would like to contact you", | ||||
|     "warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", | ||||
|     "warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", | ||||
|     "wouldliketocontactyou": "Would like to contact you", | ||||
|     "you": "You:", | ||||
|     "youhaveblockeduser": "You have blocked this user in the past", | ||||
|     "youhaveblockeduser": "You have blocked this user.", | ||||
|     "yourcontactrequestpending": "Your contact request is pending with {{$a}}" | ||||
| } | ||||
| @ -109,11 +109,20 @@ export class AddonMessagesModule { | ||||
|                     messagesProvider.invalidateDiscussionsCache(notification.site).finally(() => { | ||||
|                         // Check if group messaging is enabled, to determine which page should be loaded.
 | ||||
|                         messagesProvider.isGroupMessagingEnabledInSite(notification.site).then((enabled) => { | ||||
|                             const pageParams: any = {}; | ||||
|                             let pageName = 'AddonMessagesIndexPage'; | ||||
|                             if (enabled) { | ||||
|                                 pageName = 'AddonMessagesGroupConversationsPage'; | ||||
|                             } | ||||
|                             linkHelper.goInSite(undefined, pageName, undefined, notification.site); | ||||
| 
 | ||||
|                             // Check if we have enough information to open the conversation.
 | ||||
|                             if (notification.convid && enabled) { | ||||
|                                 pageParams.conversationId = Number(notification.convid); | ||||
|                             } else if (notification.userfromid) { | ||||
|                                 pageParams.discussionUserId = Number(notification.userfromid); | ||||
|                             } | ||||
| 
 | ||||
|                             linkHelper.goInSite(undefined, pageName, pageParams, notification.site); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
| @ -27,7 +27,7 @@ | ||||
|             <ion-avatar core-user-avatar [user]="member" [linkProfile]="false" [checkOnline]="member.showonlinestatus" item-start></ion-avatar> | ||||
|             <h2> | ||||
|                 <core-format-text [text]="member.fullname"></core-format-text> | ||||
|                 <core-icon name="fa-ban" *ngIf="member.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon> | ||||
|                 <core-icon name="fa-ban" *ngIf="member.isblocked" [label]="'addon.messages.contactblocked' | translate"></core-icon> | ||||
|             </h2> | ||||
|         </a> | ||||
| 
 | ||||
|  | ||||
| @ -352,8 +352,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         } | ||||
| 
 | ||||
|         // Check if we are at the bottom to scroll it after render.
 | ||||
|         this.scrollBottom = this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) === | ||||
|             this.domUtils.getContentHeight(this.content); | ||||
|         // Use a 5px error margin because in iOS there is 1px difference for some reason.
 | ||||
|         this.scrollBottom = Math.abs(this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) - | ||||
|             this.domUtils.getContentHeight(this.content)) < 5; | ||||
| 
 | ||||
|         if (this.messagesBeingSent > 0) { | ||||
|             // Ignore polling due to a race condition.
 | ||||
|  | ||||
| @ -100,7 +100,7 @@ | ||||
| 
 | ||||
|         <h2> | ||||
|             <core-format-text [text]="conversation.name"></core-format-text> | ||||
|             <core-icon name="fa-ban" *ngIf="conversation.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon> | ||||
|             <core-icon name="fa-ban" *ngIf="conversation.isblocked" [label]="'addon.messages.contactblocked' | translate"></core-icon> | ||||
|         </h2> | ||||
|         <ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount"> | ||||
|             <ion-badge *ngIf="conversation.unreadcount > 0">{{ conversation.unreadcount }}</ion-badge> | ||||
|  | ||||
| @ -70,6 +70,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|     protected siteId: string; | ||||
|     protected currentUserId: number; | ||||
|     protected conversationId: number; | ||||
|     protected discussionUserId: number; | ||||
|     protected newMessagesObserver: any; | ||||
|     protected pushObserver: any; | ||||
|     protected appResumeSubscription: any; | ||||
| @ -89,7 +90,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|         this.loadingString = translate.instant('core.loading'); | ||||
|         this.siteId = sitesProvider.getCurrentSiteId(); | ||||
|         this.currentUserId = sitesProvider.getCurrentSiteUserId(); | ||||
|         // Conversation to load.
 | ||||
|         this.conversationId = navParams.get('conversationId') || false; | ||||
|         this.discussionUserId = !this.conversationId && (navParams.get('discussionUserId') || false); | ||||
| 
 | ||||
|         // Update conversations when new message is received.
 | ||||
|         this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => { | ||||
| @ -138,8 +141,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|                     conversation.unreadcount = 0; | ||||
| 
 | ||||
|                     // Conversations changed, invalidate them and refresh unread counts.
 | ||||
|                     this.messagesProvider.invalidateConversations(); | ||||
|                     this.messagesProvider.refreshUnreadConversationCounts(); | ||||
|                     this.messagesProvider.invalidateConversations(this.siteId); | ||||
|                     this.messagesProvider.refreshUnreadConversationCounts(this.siteId); | ||||
|                 } | ||||
|             } | ||||
|         }, this.siteId); | ||||
| @ -213,13 +216,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|      * Component loaded. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         if (this.conversationId) { | ||||
|         if (this.conversationId || this.discussionUserId) { | ||||
|             // There is a discussion to load, open the discussion in a new state.
 | ||||
|             this.gotoConversation(this.conversationId); | ||||
|             this.gotoConversation(this.conversationId, this.discussionUserId); | ||||
|         } | ||||
| 
 | ||||
|         this.fetchData().then(() => { | ||||
|             if (!this.conversationId && this.splitviewCtrl.isOn()) { | ||||
|             if (!this.conversationId && !this.discussionUserId && this.splitviewCtrl.isOn()) { | ||||
|                 // Load the first conversation.
 | ||||
|                 let conversation; | ||||
|                 const expandedOption = this.getExpandedOption(); | ||||
| @ -248,12 +251,12 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|         const promises = []; | ||||
| 
 | ||||
|         promises.push(this.fetchConversationCounts()); | ||||
|         promises.push(this.messagesProvider.getContactRequestsCount());  // View updated by the event observer.
 | ||||
|         promises.push(this.messagesProvider.getContactRequestsCount(this.siteId));  // View updated by the event observer.
 | ||||
| 
 | ||||
|         return Promise.all(promises).then(() => { | ||||
|             if (typeof this.favourites.expanded == 'undefined') { | ||||
|                 // The expanded status hasn't been initialized. Do it now.
 | ||||
|                 if (this.conversationId) { | ||||
|                 if (this.conversationId || this.discussionUserId) { | ||||
|                     // A certain conversation should be opened.
 | ||||
|                     // We don't know which option it belongs to, so we need to fetch the data for all of them.
 | ||||
|                     const promises = []; | ||||
| @ -264,7 +267,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|                     return Promise.all(promises).then(() => { | ||||
|                         // All conversations have been loaded, find the one we need to load and expand its option.
 | ||||
|                         const conversation = this.findConversation(this.conversationId); | ||||
|                         const conversation = this.findConversation(this.conversationId, this.discussionUserId); | ||||
|                         if (conversation) { | ||||
|                             const option = this.getConversationOption(conversation); | ||||
| 
 | ||||
| @ -320,7 +323,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|             promises.push(this.fetchConversationCounts()); | ||||
|             if (refreshUnreadCounts) { | ||||
|                 promises.push(this.messagesProvider.refreshUnreadConversationCounts()); // View updated by the event observer.
 | ||||
|                 // View updated by event observer.
 | ||||
|                 promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId)); | ||||
|             } | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
| @ -344,10 +348,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|             offlineMessages; | ||||
| 
 | ||||
|         // Get the conversations and, if needed, the offline messages. Always try to get the latest data.
 | ||||
|         promises.push(this.messagesProvider.invalidateConversations().catch(() => { | ||||
|         promises.push(this.messagesProvider.invalidateConversations(this.siteId).catch(() => { | ||||
|             // Shouldn't happen.
 | ||||
|         }).then(() => { | ||||
|             return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom); | ||||
|             return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom, this.siteId); | ||||
|         }).then((result) => { | ||||
|             data = result; | ||||
|         })); | ||||
| @ -359,7 +363,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|             promises.push(this.fetchConversationCounts()); | ||||
|             if (refreshUnreadCounts) { | ||||
|                 promises.push(this.messagesProvider.refreshUnreadConversationCounts()); // View updated by the event observer.
 | ||||
|                 // View updated by the event observer.
 | ||||
|                 promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -389,10 +394,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|      */ | ||||
|     protected fetchConversationCounts(): Promise<void> { | ||||
|         // Always try to get the latest data.
 | ||||
|         return this.messagesProvider.invalidateConversationCounts().catch(() => { | ||||
|         return this.messagesProvider.invalidateConversationCounts(this.siteId).catch(() => { | ||||
|             // Shouldn't happen.
 | ||||
|         }).then(() => { | ||||
|             return this.messagesProvider.getConversationCounts(); | ||||
|             return this.messagesProvider.getConversationCounts(this.siteId); | ||||
|         }).then((counts) => { | ||||
|             this.favourites.count = counts.favourites; | ||||
|             this.individual.count = counts.individual; | ||||
| @ -607,7 +612,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|     refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise<any> { | ||||
|         // Don't invalidate conversations and so, they always try to get latest data.
 | ||||
|         const promises = [ | ||||
|             this.messagesProvider.invalidateContactRequestsCountCache() | ||||
|             this.messagesProvider.invalidateContactRequestsCountCache(this.siteId) | ||||
|         ]; | ||||
| 
 | ||||
|         return this.utils.allPromises(promises).finally(() => { | ||||
|  | ||||
| @ -34,7 +34,7 @@ | ||||
|         <ion-avatar item-start core-user-avatar [user]="result" [checkOnline]="true" [linkProfile]="false"></ion-avatar> | ||||
|         <h2> | ||||
|             <core-format-text [text]="result.fullname"></core-format-text> | ||||
|             <core-icon name="fa-ban" *ngIf="result.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon> | ||||
|             <core-icon name="fa-ban" *ngIf="result.isblocked" [label]="'addon.messages.contactblocked' | translate"></core-icon> | ||||
|         </h2> | ||||
|         <ion-note *ngIf="result.lastmessagedate > 0"> | ||||
|             {{result.lastmessagedate | coreDateDayOrTime}} | ||||
|  | ||||
| @ -32,6 +32,7 @@ | ||||
|             </ion-list> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <!-- Notifications. --> | ||||
|         <ng-container *ngIf="preferences"> | ||||
|             <div *ngFor="let component of preferences.components"> | ||||
|                 <ion-card list *ngFor="let notification of component.notifications"> | ||||
| @ -90,5 +91,20 @@ | ||||
|                 </ion-card> | ||||
|             </div> | ||||
|         </ng-container> | ||||
| 
 | ||||
|         <!-- General settings. --> | ||||
|         <ion-card> | ||||
|             <ion-list text-wrap> | ||||
|                 <ion-item-divider>{{ 'core.settings.general' | translate }}</ion-item-divider> | ||||
|                 <ion-item text-wrap> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.messages.useentertosend' | translate }}</h2> | ||||
|                         <p *ngIf="isDesktop && !isMac">{{ 'addon.messages.useentertosenddescdesktop' | translate }}</p> | ||||
|                         <p *ngIf="isDesktop && isMac">{{ 'addon.messages.useentertosenddescmac' | translate }}</p> | ||||
|                     </ion-label> | ||||
|                     <ion-toggle [(ngModel)]="sendOnEnter" (ngModelChange)="sendOnEnterChanged()"></ion-toggle> | ||||
|                 </ion-item> | ||||
|             </ion-list> | ||||
|         </ion-card> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
|  | ||||
| @ -16,8 +16,12 @@ import { Component, OnDestroy } from '@angular/core'; | ||||
| import { IonicPage } from 'ionic-angular'; | ||||
| import { AddonMessagesProvider } from '../../providers/messages'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreConfigProvider } from '@providers/config'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreConstants } from '@core/constants'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the messages settings page. | ||||
| @ -39,16 +43,27 @@ export class AddonMessagesSettingsPage implements OnDestroy { | ||||
|     courseMemberValue = AddonMessagesProvider.MESSAGE_PRIVACY_COURSEMEMBER; | ||||
|     siteValue = AddonMessagesProvider.MESSAGE_PRIVACY_SITE; | ||||
|     groupMessagingEnabled: boolean; | ||||
|     sendOnEnter: boolean; | ||||
|     isDesktop: boolean; | ||||
|     isMac: boolean; | ||||
| 
 | ||||
|     protected previousContactableValue: number | boolean; | ||||
| 
 | ||||
|     constructor(private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, | ||||
|             private userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider) { | ||||
|             private userProvider: CoreUserProvider, private sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, | ||||
|             private configProvider: CoreConfigProvider, private eventsProvider: CoreEventsProvider) { | ||||
| 
 | ||||
|         const currentSite = sitesProvider.getCurrentSite(); | ||||
|         this.advancedContactable = currentSite && currentSite.isVersionGreaterEqualThan('3.6'); | ||||
|         this.allowSiteMessaging = currentSite && currentSite.canUseAdvancedFeature('messagingallusers'); | ||||
|         this.groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); | ||||
| 
 | ||||
|         this.configProvider.get(CoreConstants.SETTINGS_SEND_ON_ENTER, !appProvider.isMobile()).then((sendOnEnter) => { | ||||
|             this.sendOnEnter = !!sendOnEnter; | ||||
|         }); | ||||
| 
 | ||||
|         this.isDesktop = !appProvider.isMobile(); | ||||
|         this.isMac = appProvider.isMac(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -233,6 +248,15 @@ export class AddonMessagesSettingsPage implements OnDestroy { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     sendOnEnterChanged(): void { | ||||
|         // Save the value.
 | ||||
|         this.configProvider.set(CoreConstants.SETTINGS_SEND_ON_ENTER, this.sendOnEnter ? 1 : 0); | ||||
| 
 | ||||
|         // Notify the app.
 | ||||
|         this.eventsProvider.trigger(CoreEventsProvider.SEND_ON_ENTER_CHANGED, {sendOnEnter: !!this.sendOnEnter}, | ||||
|                 this.sitesProvider.getCurrentSiteId()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Page destroyed. | ||||
|      */ | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| 
 | ||||
| @ -29,65 +29,69 @@ export class AddonMessagesOfflineProvider { | ||||
|     // Variables for database.
 | ||||
|     static MESSAGES_TABLE = 'addon_messages_offline_messages'; // When group messaging isn't available or a new conversation starts.
 | ||||
|     static CONVERSATION_MESSAGES_TABLE = 'addon_messages_offline_conversation_messages'; // Conversation messages.
 | ||||
|     protected tablesSchema = [ | ||||
|         { | ||||
|             name: AddonMessagesOfflineProvider.MESSAGES_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'touserid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'useridfrom', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'smallmessage', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'deviceoffline', // If message was stored because device was offline.
 | ||||
|                     type: 'INTEGER' | ||||
|                 } | ||||
|             ], | ||||
|             primaryKeys: ['touserid', 'smallmessage', 'timecreated'] | ||||
|         }, | ||||
|         { | ||||
|             name: AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'conversationid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'text', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'deviceoffline', // If message was stored because device was offline.
 | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'conversation', // Data about the conversation.
 | ||||
|                     type: 'TEXT' | ||||
|                 } | ||||
|             ], | ||||
|             primaryKeys: ['conversationid', 'text', 'timecreated'] | ||||
|         } | ||||
|     ]; | ||||
|     protected siteSchema: CoreSiteSchema = { | ||||
|         name: 'AddonMessagesOfflineProvider', | ||||
|         version: 1, | ||||
|         tables: [ | ||||
|             { | ||||
|                 name: AddonMessagesOfflineProvider.MESSAGES_TABLE, | ||||
|                 columns: [ | ||||
|                     { | ||||
|                         name: 'touserid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'useridfrom', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'smallmessage', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'timecreated', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'deviceoffline', // If message was stored because device was offline.
 | ||||
|                         type: 'INTEGER' | ||||
|                     } | ||||
|                 ], | ||||
|                 primaryKeys: ['touserid', 'smallmessage', 'timecreated'] | ||||
|             }, | ||||
|             { | ||||
|                 name: AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, | ||||
|                 columns: [ | ||||
|                     { | ||||
|                         name: 'conversationid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'text', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'timecreated', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'deviceoffline', // If message was stored because device was offline.
 | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'conversation', // Data about the conversation.
 | ||||
|                         type: 'TEXT' | ||||
|                     } | ||||
|                 ], | ||||
|                 primaryKeys: ['conversationid', 'text', 'timecreated'] | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, | ||||
|             private textUtils: CoreTextUtilsProvider) { | ||||
|         this.logger = logger.getInstance('AddonMessagesOfflineProvider'); | ||||
|         this.sitesProvider.createTablesFromSchema(this.tablesSchema); | ||||
|         this.sitesProvider.registerSiteSchema(this.siteSchema); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="assign && (description || (assign.introattachments && assign.introattachments.length))" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></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="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline"  [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
| @ -31,7 +32,15 @@ | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- User can view all submissions (teacher). --> | ||||
|     <ion-card *ngIf="assign && canViewAllSubmissions" class="core-list-align-detail-right"> | ||||
|     <ion-list *ngIf="assign && canViewAllSubmissions" class="core-list-align-detail-right with-borders"> | ||||
|         <ion-item text-wrap *ngIf="(groupInfo.separateGroups || groupInfo.visibleGroups)"> | ||||
|             <ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label> | ||||
|             <ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label> | ||||
|             <ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-assign-groupslabel" interface="action-sheet"> | ||||
|                 <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option> | ||||
|             </ion-select> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <ion-item text-wrap *ngIf="timeRemaining"> | ||||
|             <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> | ||||
|             <p>{{ timeRemaining }}</p> | ||||
| @ -79,7 +88,7 @@ | ||||
|             <ion-icon name="information-circle"></ion-icon> | ||||
|             {{ 'addon.mod_assign.ungroupedusers' | translate }} | ||||
|         </div> | ||||
|     </ion-card> | ||||
|     </ion-list> | ||||
| 
 | ||||
|     <!-- If it's a student, display his submission. --> | ||||
|     <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" [moduleId]="module.id"></addon-mod-assign-submission> | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
| 
 | ||||
| import { Component, Optional, Injector, ViewChild } from '@angular/core'; | ||||
| import { Content, NavController } from 'ionic-angular'; | ||||
| import { CoreGroupsProvider } from '@providers/groups'; | ||||
| import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; | ||||
| import { AddonModAssignProvider } from '../../providers/assign'; | ||||
| @ -45,6 +45,12 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|     summary: any; // The summary.
 | ||||
|     needsGradingAvalaible: boolean; // Whether we can see the submissions that need grading.
 | ||||
| 
 | ||||
|     groupInfo: CoreGroupInfo = { | ||||
|         groups: [], | ||||
|         separateGroups: false, | ||||
|         visibleGroups: false | ||||
|     }; | ||||
| 
 | ||||
|     // Status.
 | ||||
|     submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED; | ||||
|     submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT; | ||||
| @ -193,15 +199,13 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|                     } | ||||
| 
 | ||||
|                     // Check if groupmode is enabled to avoid showing wrong numbers.
 | ||||
|                     return this.groupsProvider.activityHasGroups(this.assign.cmid).then((hasGroups) => { | ||||
|                         this.showNumbers = !hasGroups; | ||||
|                     return this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false).then((groupInfo) => { | ||||
|                         this.groupInfo = groupInfo; | ||||
|                         this.showNumbers = groupInfo.groups.length == 0 || | ||||
|                             this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.5'); | ||||
| 
 | ||||
|                         return this.assignProvider.getSubmissionStatus(this.assign.id).then((response) => { | ||||
|                             this.summary = response.gradingsummary; | ||||
| 
 | ||||
|                             this.needsGradingAvalaible = response.gradingsummary.submissionsneedgradingcount > 0 && | ||||
|                                     this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.2'); | ||||
|                         }); | ||||
|                         return this.setGroup(this.group || (groupInfo.groups && groupInfo.groups[0] && groupInfo.groups[0].id) || | ||||
|                             0); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
| @ -222,6 +226,23 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set group to see the summary. | ||||
|      * | ||||
|      * @param  {number}       groupId Group ID. | ||||
|      * @return {Promise<any>}         Resolved when done. | ||||
|      */ | ||||
|     setGroup(groupId: number): Promise<any> { | ||||
|         this.group = groupId; | ||||
| 
 | ||||
|         return this.assignProvider.getSubmissionStatus(this.assign.id, undefined, this.group).then((response) => { | ||||
|             this.summary = response.gradingsummary; | ||||
| 
 | ||||
|             this.needsGradingAvalaible = response.gradingsummary && response.gradingsummary.submissionsneedgradingcount > 0 && | ||||
|                     this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.2'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to view a list of submissions. | ||||
|      * | ||||
| @ -232,6 +253,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|         if (typeof status == 'undefined') { | ||||
|             this.navCtrl.push('AddonModAssignSubmissionListPage', { | ||||
|                 courseId: this.courseId, | ||||
|                 groupId: this.group || 0, | ||||
|                 moduleId: this.module.id, | ||||
|                 moduleName: this.moduleName | ||||
|             }); | ||||
| @ -239,6 +261,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|             this.navCtrl.push('AddonModAssignSubmissionListPage', { | ||||
|                 status: status, | ||||
|                 courseId: this.courseId, | ||||
|                 groupId: this.group || 0, | ||||
|                 moduleId: this.module.id, | ||||
|                 moduleName: this.moduleName | ||||
|             }); | ||||
| @ -273,7 +296,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|             promises.push(this.assignProvider.invalidateAllSubmissionData(this.assign.id)); | ||||
| 
 | ||||
|             if (this.canViewAllSubmissions) { | ||||
|                 promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id)); | ||||
|                 promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, undefined, this.group)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -215,6 +215,12 @@ | ||||
|                     <p *ngIf="feedback.gradeddate">{{ feedback.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                 </a> | ||||
| 
 | ||||
|                 <!-- Grader is hidden, display only the grade date. --> | ||||
|                 <ion-item text-wrap *ngIf="!grader && feedback.gradeddate"> | ||||
|                     <h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2> | ||||
|                     <p>{{ feedback.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Warning message if cannot save grades. --> | ||||
|                 <div *ngIf="isGrading && !canSaveGrades" class="core-warning-card" icon-start> | ||||
|                     <ion-icon name="warning"></ion-icon> | ||||
|  | ||||
| @ -338,7 +338,8 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); | ||||
|         if (this.assign) { | ||||
|             promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, this.submitId, !!this.blindId)); | ||||
|             promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, this.submitId, undefined, | ||||
|                 !!this.blindId)); | ||||
|             promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(this.assign.id)); | ||||
|             promises.push(this.assignProvider.invalidateListParticipantsData(this.assign.id)); | ||||
|         } | ||||
| @ -408,7 +409,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|             return Promise.all(promises); | ||||
|         }).then(() => { | ||||
|             // Get submission status.
 | ||||
|             return this.assignProvider.getSubmissionStatus(this.assign.id, this.submitId, isBlind); | ||||
|             return this.assignProvider.getSubmissionStatusWithRetry(this.assign, this.submitId, undefined, isBlind); | ||||
|         }).then((response) => { | ||||
| 
 | ||||
|             const promises = []; | ||||
| @ -485,12 +486,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|             this.feedback = feedback; | ||||
| 
 | ||||
|             // If we have data about the grader, get its profile.
 | ||||
|             if (feedback.grade && feedback.grade.grader) { | ||||
|             if (feedback.grade && feedback.grade.grader > 0) { | ||||
|                 this.userProvider.getProfile(feedback.grade.grader, this.courseId).then((profile) => { | ||||
|                     this.grader = profile; | ||||
|                 }).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }); | ||||
|             } else { | ||||
|                 delete this.grader; | ||||
|             } | ||||
| 
 | ||||
|             // Check if the grade uses advanced grading.
 | ||||
|  | ||||
| @ -121,9 +121,11 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { | ||||
|         }).then(() => { | ||||
| 
 | ||||
|             // Get submission status. Ignore cache to get the latest data.
 | ||||
|             return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, this.isBlind, false, true).catch((err) => { | ||||
|             return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind, false, true) | ||||
|                     .catch((err) => { | ||||
|                 // Cannot connect. Get cached data.
 | ||||
|                 return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, this.isBlind).then((response) => { | ||||
|                 return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind) | ||||
|                         .then((response) => { | ||||
|                     const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt); | ||||
| 
 | ||||
|                     // Check if the user can edit it in offline.
 | ||||
| @ -303,6 +305,9 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { | ||||
|                 } | ||||
| 
 | ||||
|                 return promise.then(() => { | ||||
|                     // Clear temporary data from plugins.
 | ||||
|                     return this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData); | ||||
|                 }).then(() => { | ||||
|                     // Submission saved, trigger event.
 | ||||
|                     const params = { | ||||
|                         assignmentId: this.assign.id, | ||||
|  | ||||
| @ -15,10 +15,17 @@ | ||||
|             </core-empty-box> | ||||
| 
 | ||||
|             <ion-list> | ||||
|                 <ion-item text-wrap *ngIf="(groupInfo.separateGroups || groupInfo.visibleGroups)"> | ||||
|                     <ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label> | ||||
|                     <ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label> | ||||
|                     <ion-select [(ngModel)]="groupId" (ionChange)="setGroup(groupId)" aria-labelledby="addon-assign-groupslabel" interface="action-sheet"> | ||||
|                         <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option> | ||||
|                     </ion-select> | ||||
|                 </ion-item> | ||||
|                 <!-- List of submissions. --> | ||||
|                 <ng-container *ngFor="let submission of submissions"> | ||||
|                     <a ion-item text-wrap (click)="loadSubmission(submission)" [class.core-split-item-selected]="submission.id == selectedSubmissionId"> | ||||
|                         <ion-avatar core-user-avatar [user]="submission" item-start></ion-avatar> | ||||
|                     <a ion-item text-wrap (click)="loadSubmission(submission)" [class.core-split-item-selected]="submission.submitid == selectedSubmissionId"> | ||||
|                         <ion-avatar core-user-avatar [user]="submission" [linkProfile]="false" item-start></ion-avatar> | ||||
|                         <h2 *ngIf="submission.userfullname">{{submission.userfullname}}</h2> | ||||
|                         <h2 *ngIf="!submission.userfullname">{{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}</h2> | ||||
|                         <p *ngIf="assign.teamsubmission"> | ||||
|  | ||||
| @ -18,6 +18,7 @@ import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | ||||
| import { AddonModAssignProvider } from '../../providers/assign'; | ||||
| import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; | ||||
| import { AddonModAssignHelperProvider } from '../../providers/helper'; | ||||
| @ -40,19 +41,28 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|     loaded: boolean; // Whether data has been loaded.
 | ||||
|     haveAllParticipants: boolean; // Whether all participants have been loaded.
 | ||||
|     selectedSubmissionId: number; // Selected submission ID.
 | ||||
|     groupId = 0; // Group ID to show.
 | ||||
| 
 | ||||
|     groupInfo: CoreGroupInfo = { | ||||
|         groups: [], | ||||
|         separateGroups: false, | ||||
|         visibleGroups: false | ||||
|     }; | ||||
| 
 | ||||
|     protected moduleId: number; // Module ID the submission belongs to.
 | ||||
|     protected courseId: number; // Course ID the assignment belongs to.
 | ||||
|     protected selectedStatus: string; // The status to see.
 | ||||
|     protected gradedObserver; // Observer to refresh data when a grade changes.
 | ||||
|     protected submissionsData: any; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, | ||||
|             protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService, | ||||
|             protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, | ||||
|             protected assignHelper: AddonModAssignHelperProvider) { | ||||
|             protected assignHelper: AddonModAssignHelperProvider, protected groupsProvider: CoreGroupsProvider) { | ||||
| 
 | ||||
|         this.moduleId = navParams.get('moduleId'); | ||||
|         this.courseId = navParams.get('courseId'); | ||||
|         this.groupId = navParams.get('groupId'); | ||||
|         this.selectedStatus = navParams.get('status'); | ||||
| 
 | ||||
|         if (this.selectedStatus) { | ||||
| @ -98,15 +108,11 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchAssignment(): Promise<any> { | ||||
|         let participants, | ||||
|             submissionsData, | ||||
|             grades; | ||||
| 
 | ||||
|         // Get assignment data.
 | ||||
|         return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { | ||||
|             this.title = assign.name || this.title; | ||||
|             this.assign = assign; | ||||
|             this.haveAllParticipants = true; | ||||
| 
 | ||||
|             // Get assignment submissions.
 | ||||
|             return this.assignProvider.getSubmissions(assign.id); | ||||
| @ -116,15 +122,39 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|                 return Promise.reject(null); | ||||
|             } | ||||
| 
 | ||||
|             submissionsData = data; | ||||
|             this.submissionsData = data; | ||||
| 
 | ||||
|             // Get the participants.
 | ||||
|             return this.assignHelper.getParticipants(this.assign).then((parts) => { | ||||
|                 this.haveAllParticipants = true; | ||||
|                 participants = parts; | ||||
|             }).catch(() => { | ||||
|                 this.haveAllParticipants = false; | ||||
|             // Check if groupmode is enabled to avoid showing wrong numbers.
 | ||||
|             return this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false).then((groupInfo) => { | ||||
|                 this.groupInfo = groupInfo; | ||||
| 
 | ||||
|                 return this.setGroup(this.groupId || (groupInfo.groups && groupInfo.groups[0] && groupInfo.groups[0].id) || 0); | ||||
|             }); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set group to see the summary. | ||||
|      * | ||||
|      * @param  {number}       groupId Group ID. | ||||
|      * @return {Promise<any>}         Resolved when done. | ||||
|      */ | ||||
|     setGroup(groupId: number): Promise<any> { | ||||
|         let participants, | ||||
|             grades; | ||||
| 
 | ||||
|         this.groupId = groupId; | ||||
| 
 | ||||
|         this.haveAllParticipants = true; | ||||
| 
 | ||||
|         // Get the participants.
 | ||||
|         return this.assignHelper.getParticipants(this.assign, this.groupId).then((parts) => { | ||||
|             this.haveAllParticipants = true; | ||||
|             participants = parts; | ||||
|         }).catch(() => { | ||||
|             this.haveAllParticipants = false; | ||||
|         }).then(() => { | ||||
|             if (!this.assign.markingworkflow) { | ||||
|                 // Get assignment grades only if workflow is not enabled to check grading date.
 | ||||
| @ -134,16 +164,16 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|             } | ||||
|         }).then(() => { | ||||
|             // We want to show the user data on each submission.
 | ||||
|             return this.assignProvider.getSubmissionsUserData(submissionsData.submissions, this.courseId, this.assign.id, | ||||
|             return this.assignProvider.getSubmissionsUserData(this.submissionsData.submissions, this.courseId, this.assign.id, | ||||
|                     this.assign.blindmarking && !this.assign.revealidentities, participants); | ||||
|         }).then((submissions) => { | ||||
| 
 | ||||
|             // Filter the submissions to get only the ones with the right status and add some extra data.
 | ||||
|             const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING, | ||||
|                 searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus, | ||||
|                 promises = []; | ||||
|                 promises = [], | ||||
|                 showSubmissions = []; | ||||
| 
 | ||||
|             this.submissions = []; | ||||
|             submissions.forEach((submission) => { | ||||
|                 if (!searchStatus || searchStatus == submission.status) { | ||||
|                     promises.push(this.assignOfflineProvider.getSubmissionGrade(this.assign.id, submission.userid).catch(() => { | ||||
| @ -203,15 +233,15 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|                                 submission.gradingStatusTranslationId = false; | ||||
|                             } | ||||
| 
 | ||||
|                             this.submissions.push(submission); | ||||
|                             showSubmissions.push(submission); | ||||
|                         }); | ||||
|                     })); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); | ||||
|             return Promise.all(promises).then(() => { | ||||
|                 this.submissions = showSubmissions; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -221,12 +251,12 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|      * @param {any} submission The submission to load. | ||||
|      */ | ||||
|     loadSubmission(submission: any): void { | ||||
|         if (this.selectedSubmissionId === submission.id && this.splitviewCtrl.isOn()) { | ||||
|         if (this.selectedSubmissionId === submission.submitid && this.splitviewCtrl.isOn()) { | ||||
|             // Already selected.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.selectedSubmissionId = submission.id; | ||||
|         this.selectedSubmissionId = submission.submitid; | ||||
| 
 | ||||
|         this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', { | ||||
|             courseId: this.courseId, | ||||
|  | ||||
| @ -132,7 +132,8 @@ export class AddonModAssignSubmissionReviewPage implements OnInit { | ||||
|         if (this.assign) { | ||||
|             promises.push(this.assignProvider.invalidateSubmissionData(this.assign.id)); | ||||
|             promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(this.assign.id)); | ||||
|             promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, this.submitId, this.blindMarking)); | ||||
|             promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, this.submitId, undefined, | ||||
|                 this.blindMarking)); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.all(promises).finally(() => { | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreFileProvider } from '@providers/file'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| 
 | ||||
| @ -30,105 +30,109 @@ export class AddonModAssignOfflineProvider { | ||||
|     // Variables for database.
 | ||||
|     static SUBMISSIONS_TABLE = 'addon_mod_assign_submissions'; | ||||
|     static SUBMISSIONS_GRADES_TABLE = 'addon_mod_assign_submissions_grading'; | ||||
|     protected tablesSchema = [ | ||||
|         { | ||||
|             name: AddonModAssignOfflineProvider.SUBMISSIONS_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'assignid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'userid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'plugindata', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'onlinetimemodified', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'submitted', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'submissionstatement', | ||||
|                     type: 'INTEGER' | ||||
|                 } | ||||
|             ], | ||||
|             primaryKeys: ['assignid', 'userid'] | ||||
|         }, | ||||
|         { | ||||
|             name: AddonModAssignOfflineProvider.SUBMISSIONS_GRADES_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'assignid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'courseid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'userid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'grade', | ||||
|                     type: 'REAL' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'attemptnumber', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'addattempt', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'workflowstate', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'applytoall', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'outcomes', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'plugindata', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timemodified', | ||||
|                     type: 'INTEGER' | ||||
|                 } | ||||
|             ], | ||||
|             primaryKeys: ['assignid', 'userid'] | ||||
|         } | ||||
|     ]; | ||||
|     protected siteSchema: CoreSiteSchema = { | ||||
|         name: 'AddonModAssignOfflineProvider', | ||||
|         version: 1, | ||||
|         tables: [ | ||||
|             { | ||||
|                 name: AddonModAssignOfflineProvider.SUBMISSIONS_TABLE, | ||||
|                 columns: [ | ||||
|                     { | ||||
|                         name: 'assignid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'courseid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'userid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'plugindata', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'onlinetimemodified', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'timecreated', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'timemodified', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'submitted', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'submissionstatement', | ||||
|                         type: 'INTEGER' | ||||
|                     } | ||||
|                 ], | ||||
|                 primaryKeys: ['assignid', 'userid'] | ||||
|             }, | ||||
|             { | ||||
|                 name: AddonModAssignOfflineProvider.SUBMISSIONS_GRADES_TABLE, | ||||
|                 columns: [ | ||||
|                     { | ||||
|                         name: 'assignid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'courseid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'userid', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'grade', | ||||
|                         type: 'REAL' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'attemptnumber', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'addattempt', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'workflowstate', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'applytoall', | ||||
|                         type: 'INTEGER' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'outcomes', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'plugindata', | ||||
|                         type: 'TEXT' | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'timemodified', | ||||
|                         type: 'INTEGER' | ||||
|                     } | ||||
|                 ], | ||||
|                 primaryKeys: ['assignid', 'userid'] | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, | ||||
|             private fileProvider: CoreFileProvider, private timeUtils: CoreTimeUtilsProvider) { | ||||
|         this.logger = logger.getInstance('AddonModAssignOfflineProvider'); | ||||
|         this.sitesProvider.createTablesFromSchema(this.tablesSchema); | ||||
|         this.sitesProvider.registerSiteSchema(this.siteSchema); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -23,6 +23,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||
| import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; | ||||
| import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||
| import { AddonModAssignProvider } from './assign'; | ||||
| @ -61,7 +62,8 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|             private courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, | ||||
|             private assignProvider: AddonModAssignProvider, private assignOfflineProvider: AddonModAssignOfflineProvider, | ||||
|             private utils: CoreUtilsProvider, private submissionDelegate: AddonModAssignSubmissionDelegate, | ||||
|             private gradesHelper: CoreGradesHelperProvider, timeUtils: CoreTimeUtilsProvider) { | ||||
|             private gradesHelper: CoreGradesHelperProvider, timeUtils: CoreTimeUtilsProvider, | ||||
|             private logHelper: CoreCourseLogHelperProvider) { | ||||
| 
 | ||||
|         super('AddonModAssignSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, | ||||
|                 timeUtils); | ||||
| @ -202,6 +204,9 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|             return []; | ||||
|         })); | ||||
| 
 | ||||
|         // Sync offline logs.
 | ||||
|         promises.push(this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId)); | ||||
| 
 | ||||
|         syncPromise = Promise.all(promises).then((results) => { | ||||
|             const submissions = results[0], | ||||
|                 grades = results[1]; | ||||
| @ -216,7 +221,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
| 
 | ||||
|             courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; | ||||
| 
 | ||||
|             return this.assignProvider.getAssignmentById(courseId, assignId, siteId).then((assignData) => { | ||||
|             return this.assignProvider.getAssignmentById(courseId, assignId, false, siteId).then((assignData) => { | ||||
|                 assign = assignData; | ||||
| 
 | ||||
|                 const promises = []; | ||||
| @ -270,7 +275,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|         let discardError, | ||||
|             submission; | ||||
| 
 | ||||
|         return this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId).then((status) => { | ||||
|         return this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId).then((status) => { | ||||
|             const promises = []; | ||||
| 
 | ||||
|             submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt); | ||||
| @ -305,7 +310,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|                     } | ||||
|                 }).then(() => { | ||||
|                     // Submission data sent, update cached data. No need to block the user for this.
 | ||||
|                     this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId); | ||||
|                     this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); | ||||
|                 }); | ||||
|             }).catch((error) => { | ||||
|                 if (error && this.utils.isWebServiceError(error)) { | ||||
| @ -359,7 +364,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|         const userId = offlineData.userid; | ||||
|         let discardError; | ||||
| 
 | ||||
|         return this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId).then((status) => { | ||||
|         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); | ||||
| 
 | ||||
|             if (timemodified > offlineData.timemodified) { | ||||
| @ -400,7 +405,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|                         offlineData.plugindata, siteId).then(() => { | ||||
| 
 | ||||
|                     // Grades sent, update cached data. No need to block the user for this.
 | ||||
|                     this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId); | ||||
|                     this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); | ||||
|                 }).catch((error) => { | ||||
|                     if (error && this.utils.isWebServiceError(error)) { | ||||
|                         // The WebService has thrown an error, this means it cannot be submitted. Discard the offline data.
 | ||||
|  | ||||
| @ -21,8 +21,8 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCommentsProvider } from '@core/comments/providers/comments'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { CoreGradesProvider } from '@core/grades/providers/grades'; | ||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||
| import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | ||||
| import { AddonModAssignOfflineProvider } from './assign-offline'; | ||||
| import { CoreSiteWSPreSets } from '@classes/site'; | ||||
| @ -66,9 +66,10 @@ export class AddonModAssignProvider { | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, | ||||
|             private timeUtils: CoreTimeUtilsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, | ||||
|             private userProvider: CoreUserProvider, private submissionDelegate: AddonModAssignSubmissionDelegate, | ||||
|             private submissionDelegate: AddonModAssignSubmissionDelegate, | ||||
|             private gradesProvider: CoreGradesProvider, private filepoolProvider: CoreFilepoolProvider, | ||||
|             private assignOffline: AddonModAssignOfflineProvider, private commentsProvider: CoreCommentsProvider) { | ||||
|             private assignOffline: AddonModAssignOfflineProvider, private commentsProvider: CoreCommentsProvider, | ||||
|             private logHelper: CoreCourseLogHelperProvider) { | ||||
|         this.logger = logger.getInstance('AddonModAssignProvider'); | ||||
|     } | ||||
| 
 | ||||
| @ -118,11 +119,12 @@ export class AddonModAssignProvider { | ||||
|      * | ||||
|      * @param {number} courseId Course ID the assignment belongs to. | ||||
|      * @param {number} cmId Assignment module ID. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved with the assignment. | ||||
|      */ | ||||
|     getAssignment(courseId: number, cmId: number, siteId?: string): Promise<any> { | ||||
|         return this.getAssignmentByField(courseId, 'cmid', cmId, siteId); | ||||
|     getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise<any> { | ||||
|         return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -131,19 +133,27 @@ export class AddonModAssignProvider { | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @param {string} key Name of the property to check. | ||||
|      * @param {any} value Value to search. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when the assignment is retrieved. | ||||
|      */ | ||||
|     protected getAssignmentByField(courseId: number, key: string, value: any, siteId?: string): Promise<any> { | ||||
|     protected getAssignmentByField(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) | ||||
|             : Promise<any> { | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                     courseids: [courseId], | ||||
|                     includenotenrolledcourses: 1 | ||||
|                 }, | ||||
|                 preSets = { | ||||
|                 preSets: CoreSiteWSPreSets = { | ||||
|                     cacheKey: this.getAssignmentCacheKey(courseId) | ||||
|                 }; | ||||
| 
 | ||||
|             if (ignoreCache) { | ||||
|                 preSets.getFromCache = false; | ||||
|                 preSets.emergencyCache = false; | ||||
|             } | ||||
| 
 | ||||
|             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.
 | ||||
|                 // Retry again without the param to check if the request is already cached.
 | ||||
| @ -172,11 +182,12 @@ export class AddonModAssignProvider { | ||||
|      * | ||||
|      * @param {number} courseId Course ID the assignment belongs to. | ||||
|      * @param {number} cmId Assignment instance ID. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}  Promise resolved with the assignment. | ||||
|      */ | ||||
|     getAssignmentById(courseId: number, id: number, siteId?: string): Promise<any> { | ||||
|         return this.getAssignmentByField(courseId, 'id', id, siteId); | ||||
|     getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise<any> { | ||||
|         return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -194,18 +205,24 @@ export class AddonModAssignProvider { | ||||
|      * | ||||
|      * @param {number} assignId Assignment Id. | ||||
|      * @param {number} userId User Id to be blinded. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<number>} Promise resolved with the user blind id. | ||||
|      */ | ||||
|     getAssignmentUserMappings(assignId: number, userId: number, siteId?: string): Promise<number> { | ||||
|     getAssignmentUserMappings(assignId: number, userId: number, ignoreCache?: boolean, siteId?: string): Promise<number> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                     assignmentids: [assignId] | ||||
|                 }, | ||||
|                 preSets = { | ||||
|                 preSets: CoreSiteWSPreSets = { | ||||
|                     cacheKey: this.getAssignmentUserMappingsCacheKey(assignId) | ||||
|                 }; | ||||
| 
 | ||||
|             if (ignoreCache) { | ||||
|                 preSets.getFromCache = false; | ||||
|                 preSets.emergencyCache = false; | ||||
|             } | ||||
| 
 | ||||
|             return site.read('mod_assign_get_user_mappings', params, preSets).then((response) => { | ||||
|                 // Search the user.
 | ||||
|                 if (response.assignments && response.assignments.length) { | ||||
| @ -248,18 +265,24 @@ export class AddonModAssignProvider { | ||||
|      * Returns grade information from assign_grades for the requested assignment id | ||||
|      * | ||||
|      * @param {number} assignId Assignment Id. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}   Resolved with requested info when done. | ||||
|      */ | ||||
|     getAssignmentGrades(assignId: number, siteId?: string): Promise<any> { | ||||
|     getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                     assignmentids: [assignId] | ||||
|                 }, | ||||
|                 preSets = { | ||||
|                 preSets: CoreSiteWSPreSets = { | ||||
|                     cacheKey: this.getAssignmentGradesCacheKey(assignId) | ||||
|                 }; | ||||
| 
 | ||||
|             if (ignoreCache) { | ||||
|                 preSets.getFromCache = false; | ||||
|                 preSets.emergencyCache = false; | ||||
|             } | ||||
| 
 | ||||
|             return site.read('mod_assign_get_grades', params, preSets).then((response) => { | ||||
|                 // Search the assignment.
 | ||||
|                 if (response.assignments && response.assignments.length) { | ||||
| @ -356,9 +379,13 @@ export class AddonModAssignProvider { | ||||
|      * | ||||
|      * @param {any} assign Assign. | ||||
|      * @param {any} attempt Attempt. | ||||
|      * @return {any} Submission object. | ||||
|      * @return {any} Submission object or null. | ||||
|      */ | ||||
|     getSubmissionObjectFromAttempt(assign: any, attempt: any): any { | ||||
|         if (!attempt) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         return assign.teamsubmission ? attempt.teamsubmission : attempt.submission; | ||||
|     } | ||||
| 
 | ||||
| @ -419,18 +446,26 @@ export class AddonModAssignProvider { | ||||
|      * Get an assignment submissions. | ||||
|      * | ||||
|      * @param {number} assignId Assignment id. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<{canviewsubmissions: boolean, submissions?: any[]}>} Promise resolved when done. | ||||
|      */ | ||||
|     getSubmissions(assignId: number, siteId?: string): Promise<{canviewsubmissions: boolean, submissions?: any[]}> { | ||||
|     getSubmissions(assignId: number, ignoreCache?: boolean, siteId?: string) | ||||
|             : Promise<{canviewsubmissions: boolean, submissions?: any[]}> { | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                     assignmentids: [assignId] | ||||
|                 }, | ||||
|                 preSets = { | ||||
|                 preSets: CoreSiteWSPreSets = { | ||||
|                     cacheKey: this.getSubmissionsCacheKey(assignId) | ||||
|                 }; | ||||
| 
 | ||||
|             if (ignoreCache) { | ||||
|                 preSets.getFromCache = false; | ||||
|                 preSets.emergencyCache = false; | ||||
|             } | ||||
| 
 | ||||
|             return site.read('mod_assign_get_submissions', params, preSets).then((response): any => { | ||||
|                 // Check if we can view submissions, with enough permissions.
 | ||||
|                 if (response.warnings.length > 0 && response.warnings[0].warningcode == 1) { | ||||
| @ -463,31 +498,37 @@ export class AddonModAssignProvider { | ||||
|      * Get information about an assignment submission status for a given user. | ||||
|      * | ||||
|      * @param {number} assignId Assignment instance id. | ||||
|      * @param {number} [userId] User id (empty for current user). | ||||
|      * @param {number} [userId] User Id (empty for current user). | ||||
|      * @param {number} [groupId] Group Id (empty for all participants). | ||||
|      * @param {boolean} [isBlind] If blind marking is enabled or not. | ||||
|      * @param {number} [filter=true] True to filter WS response and rewrite URLs, false otherwise. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site id (empty for current site). | ||||
|      * @return {Promise<any>} Promise always resolved with the user submission status. | ||||
|      */ | ||||
|     getSubmissionStatus(assignId: number, userId?: number, isBlind?: boolean, filter: boolean = true, ignoreCache?: boolean, | ||||
|             siteId?: string): Promise<any> { | ||||
|     getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, | ||||
|             ignoreCache?: boolean, siteId?: string): Promise<any> { | ||||
| 
 | ||||
|         userId = userId || 0; | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             groupId = site.isVersionGreaterEqualThan('3.5') ? groupId || 0 : 0; | ||||
| 
 | ||||
|             const params = { | ||||
|                     assignid: assignId, | ||||
|                     userid: userId | ||||
|                 }, | ||||
|                 preSets: CoreSiteWSPreSets = { | ||||
|                     cacheKey: this.getSubmissionStatusCacheKey(assignId, userId, isBlind), | ||||
|                     cacheKey: this.getSubmissionStatusCacheKey(assignId, userId, groupId, isBlind), | ||||
|                     getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account.
 | ||||
|                     filter: filter, | ||||
|                     rewriteurls: filter | ||||
|                 }; | ||||
| 
 | ||||
|             if (groupId) { | ||||
|                 params['groupid'] = groupId; | ||||
|             } | ||||
| 
 | ||||
|             if (ignoreCache) { | ||||
|                 preSets.getFromCache = false; | ||||
|                 preSets.emergencyCache = false; | ||||
| @ -503,21 +544,53 @@ export class AddonModAssignProvider { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get information about an assignment submission status for a given user. | ||||
|      * If the data doesn't include the user submission, retry ignoring cache. | ||||
|      * | ||||
|      * @param {any} assign Assignment. | ||||
|      * @param {number} [userId] User id (empty for current user). | ||||
|      * @param {number} [groupId] Group Id (empty for all participants). | ||||
|      * @param {boolean} [isBlind] If blind marking is enabled or not. | ||||
|      * @param {number} [filter=true] True to filter WS response and rewrite URLs, false otherwise. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site id (empty for current site). | ||||
|      * @return {Promise<any>} Promise always resolved with the user submission status. | ||||
|      */ | ||||
|     getSubmissionStatusWithRetry(assign: any, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, | ||||
|             ignoreCache?: boolean, siteId?: string): Promise<any> { | ||||
| 
 | ||||
|         return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, ignoreCache, siteId).then((response) => { | ||||
|             const userSubmission = this.getSubmissionObjectFromAttempt(assign, response.lastattempt); | ||||
| 
 | ||||
|             if (!userSubmission) { | ||||
|                 // Try again, ignoring cache.
 | ||||
|                 return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, true, siteId).catch(() => { | ||||
|                     // Error, return the first result even if it doesn't have the user submission.
 | ||||
|                     return response; | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return response; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for get submission status data WS calls. | ||||
|      * | ||||
|      * @param {number} assignId Assignment instance id. | ||||
|      * @param {number} [userId] User id (empty for current user). | ||||
|      * @param {number} [groupId] Group Id (empty for all participants). | ||||
|      * @param {number} [isBlind] If blind marking is enabled or not. | ||||
|      * @return {string} Cache key. | ||||
|      */ | ||||
|     protected getSubmissionStatusCacheKey(assignId: number, userId: number, isBlind?: boolean): string { | ||||
|     protected getSubmissionStatusCacheKey(assignId: number, userId: number, groupId?: number, isBlind?: boolean): string { | ||||
|         if (!userId) { | ||||
|             isBlind = false; | ||||
|             userId = this.sitesProvider.getCurrentSiteUserId(); | ||||
|         } | ||||
| 
 | ||||
|         return this.getSubmissionsCacheKey(assignId) + ':' + userId + ':' + (isBlind ? 1 : 0); | ||||
|         return this.getSubmissionsCacheKey(assignId) + ':' + userId + ':' + (isBlind ? 1 : 0) + ':' + groupId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -551,16 +624,21 @@ export class AddonModAssignProvider { | ||||
|      * @param {number} assignId ID of the assignment the submissions belong to. | ||||
|      * @param {boolean} [blind] Whether the user data need to be blinded. | ||||
|      * @param {any[]} [participants] List of participants in the assignment. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site id (empty for current site). | ||||
|      * @return {Promise<any[]>} Promise always resolved. Resolve param is the formatted submissions. | ||||
|      */ | ||||
|     getSubmissionsUserData(submissions: any[], courseId: number, assignId: number, blind?: boolean, participants?: any[], | ||||
|             siteId?: string): Promise<any[]> { | ||||
|             ignoreCache?: boolean, siteId?: string): Promise<any[]> { | ||||
| 
 | ||||
|         const promises = [], | ||||
|             subs = [], | ||||
|             hasParticipants = participants && participants.length > 0; | ||||
| 
 | ||||
|         if (!hasParticipants) { | ||||
|             return Promise.resolve([]); | ||||
|         } | ||||
| 
 | ||||
|         submissions.forEach((submission) => { | ||||
|             submission.submitid = submission.userid > 0 ? submission.userid : submission.blindid; | ||||
|             if (submission.submitid <= 0) { | ||||
| @ -568,42 +646,30 @@ export class AddonModAssignProvider { | ||||
|             } | ||||
| 
 | ||||
|             const participant = this.getParticipantFromUserId(participants, submission.submitid); | ||||
|             if (hasParticipants && !participant) { | ||||
|             if (!participant) { | ||||
|                 // Avoid permission denied error. Participant not found on list.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (participant) { | ||||
|                 if (!blind) { | ||||
|                     submission.userfullname = participant.fullname; | ||||
|                     submission.userprofileimageurl = participant.profileimageurl; | ||||
|                 } | ||||
|             if (!blind) { | ||||
|                 submission.userfullname = participant.fullname; | ||||
|                 submission.userprofileimageurl = participant.profileimageurl; | ||||
|             } | ||||
| 
 | ||||
|                 submission.manyGroups = !!participant.groups && participant.groups.length > 1; | ||||
|                 if (participant.groupname) { | ||||
|                     submission.groupid = participant.groupid; | ||||
|                     submission.groupname = participant.groupname; | ||||
|                 } | ||||
|             submission.manyGroups = !!participant.groups && participant.groups.length > 1; | ||||
|             if (participant.groupname) { | ||||
|                 submission.groupid = participant.groupid; | ||||
|                 submission.groupname = participant.groupname; | ||||
|             } | ||||
| 
 | ||||
|             let promise; | ||||
|             if (submission.userid > 0) { | ||||
|                 if (blind) { | ||||
|                     // Blind but not blinded! (Moodle < 3.1.1, 3.2).
 | ||||
|                     delete submission.userid; | ||||
|             if (submission.userid > 0 && blind) { | ||||
|                 // Blind but not blinded! (Moodle < 3.1.1, 3.2).
 | ||||
|                 delete submission.userid; | ||||
| 
 | ||||
|                     promise = this.getAssignmentUserMappings(assignId, submission.submitid, siteId).then((blindId) => { | ||||
|                         submission.blindid = blindId; | ||||
|                     }); | ||||
|                 } else if (!participant) { | ||||
|                     // No blind, no participant.
 | ||||
|                     promise = this.userProvider.getProfile(submission.userid, courseId, true).then((user) => { | ||||
|                         submission.userfullname = user.fullname; | ||||
|                         submission.userprofileimageurl = user.profileimageurl; | ||||
|                     }).catch(() => { | ||||
|                         // Error getting profile, resolve promise without adding any extra data.
 | ||||
|                     }); | ||||
|                 } | ||||
|                 promise = this.getAssignmentUserMappings(assignId, submission.submitid, ignoreCache, siteId).then((blindId) => { | ||||
|                     submission.blindid = blindId; | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             promise = promise || Promise.resolve(); | ||||
| @ -675,10 +741,11 @@ export class AddonModAssignProvider { | ||||
|      * | ||||
|      * @param {number} assignId Assignment id. | ||||
|      * @param {number} [groupId] Group id. If not defined, 0. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any[]>} Promise resolved with the list of participants and summary of submissions. | ||||
|      */ | ||||
|     listParticipants(assignId: number, groupId?: number, siteId?: string): Promise<any[]> { | ||||
|     listParticipants(assignId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise<any[]> { | ||||
|         groupId = groupId || 0; | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
| @ -692,10 +759,15 @@ export class AddonModAssignProvider { | ||||
|                     groupid: groupId, | ||||
|                     filter: '' | ||||
|                 }, | ||||
|                 preSets = { | ||||
|                 preSets: CoreSiteWSPreSets = { | ||||
|                     cacheKey: this.listParticipantsCacheKey(assignId, groupId) | ||||
|                 }; | ||||
| 
 | ||||
|             if (ignoreCache) { | ||||
|                 preSets.getFromCache = false; | ||||
|                 preSets.emergencyCache = false; | ||||
|             } | ||||
| 
 | ||||
|             return site.read('mod_assign_list_participants', params, preSets); | ||||
|         }); | ||||
|     } | ||||
| @ -785,7 +857,7 @@ export class AddonModAssignProvider { | ||||
|     invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         return this.getAssignment(courseId, moduleId, siteId).then((assign) => { | ||||
|         return this.getAssignment(courseId, moduleId, false, siteId).then((assign) => { | ||||
|             const promises = []; | ||||
| 
 | ||||
|             // Do not invalidate assignment data before getting assignment info, we need it!
 | ||||
| @ -830,13 +902,15 @@ export class AddonModAssignProvider { | ||||
|      * | ||||
|      * @param {number} assignId Assignment instance id. | ||||
|      * @param {number} [userId] User id (empty for current user). | ||||
|      * @param {number} [groupId] Group Id (empty for all participants). | ||||
|      * @param {boolean} [isBlind] Whether blind marking is enabled or not. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateSubmissionStatusData(assignId: number, userId?: number, isBlind?: boolean, siteId?: string): Promise<any> { | ||||
|     invalidateSubmissionStatusData(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, siteId?: string): | ||||
|             Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.invalidateWsCacheForKey(this.getSubmissionStatusCacheKey(assignId, userId, isBlind)); | ||||
|             return site.invalidateWsCacheForKey(this.getSubmissionStatusCacheKey(assignId, userId, groupId, isBlind)); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -976,13 +1050,11 @@ export class AddonModAssignProvider { | ||||
|      * @return {Promise<any>} Promise resolved when the WS call is successful. | ||||
|      */ | ||||
|     logGradingView(assignId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                 assignid: assignId | ||||
|             }; | ||||
|         const params = { | ||||
|             assignid: assignId | ||||
|         }; | ||||
| 
 | ||||
|             return site.write('mod_assign_view_grading_table', params); | ||||
|         }); | ||||
|         return this.logHelper.log('mod_assign_view_grading_table', params, AddonModAssignProvider.COMPONENT, assignId, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -993,13 +1065,11 @@ export class AddonModAssignProvider { | ||||
|      * @return {Promise<any>} Promise resolved when the WS call is successful. | ||||
|      */ | ||||
|     logView(assignId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                 assignid: assignId | ||||
|             }; | ||||
|         const params = { | ||||
|             assignid: assignId | ||||
|         }; | ||||
| 
 | ||||
|             return site.write('mod_assign_view_assign', params); | ||||
|         }); | ||||
|         return this.logHelper.log('mod_assign_view_assign', params, AddonModAssignProvider.COMPONENT, assignId, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -1022,7 +1092,7 @@ export class AddonModAssignProvider { | ||||
|         } | ||||
| 
 | ||||
|         // We need more data to decide that.
 | ||||
|         return this.getSubmissionStatus(assignId, submission.submitid, submission.blindid).then((response) => { | ||||
|         return this.getSubmissionStatus(assignId, submission.submitid, undefined, submission.blindid).then((response) => { | ||||
|             if (!response.feedback || !response.feedback.gradeddate) { | ||||
|                 // Not graded.
 | ||||
|                 return true; | ||||
|  | ||||
| @ -149,26 +149,29 @@ export class AddonModAssignHelperProvider { | ||||
|     /** | ||||
|      * List the participants for a single assignment, with some summary info about their submissions. | ||||
|      * | ||||
|      * @param {any} assign Assignment object | ||||
|      * @param {any} assign Assignment object. | ||||
|      * @param {number} [groupId] Group Id. | ||||
|      * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any[]} Promise resolved with the list of participants and summary of submissions. | ||||
|      */ | ||||
|     getParticipants(assign: any, siteId?: string): Promise<any[]> { | ||||
|     getParticipants(assign: any, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise<any[]> { | ||||
|         groupId = groupId || 0; | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Get the participants without specifying a group.
 | ||||
|         return this.assignProvider.listParticipants(assign.id, undefined, siteId).then((participants) => { | ||||
|             if (participants && participants.length > 0) { | ||||
|         return this.assignProvider.listParticipants(assign.id, groupId, ignoreCache, siteId).then((participants) => { | ||||
|             if (groupId || participants && participants.length > 0) { | ||||
|                 return participants; | ||||
|             } | ||||
| 
 | ||||
|             // If no participants returned, get participants by groups.
 | ||||
|             // If no participants returned and all groups specified, get participants by groups.
 | ||||
|             return this.groupsProvider.getActivityAllowedGroupsIfEnabled(assign.cmid, undefined, siteId).then((userGroups) => { | ||||
|                 const promises = [], | ||||
|                     participants = {}; | ||||
| 
 | ||||
|                 userGroups.forEach((userGroup) => { | ||||
|                     promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, siteId).then((parts) => { | ||||
|                     promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, ignoreCache, siteId) | ||||
|                             .then((parts) => { | ||||
|                         // Do not get repeated users.
 | ||||
|                         parts.forEach((participant) => { | ||||
|                             participants[participant.id] = participant; | ||||
|  | ||||
| @ -68,8 +68,12 @@ export class AddonModAssignModuleHandler implements CoreCourseModuleHandler { | ||||
|             title: module.name, | ||||
|             class: 'addon-mod_assign-handler', | ||||
|             showDownloadButton: true, | ||||
|             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { | ||||
|                 navCtrl.push('AddonModAssignIndexPage', {module: module, courseId: courseId}, options); | ||||
|             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void { | ||||
|                 const pageParams = {module: module, courseId: courseId}; | ||||
|                 if (params) { | ||||
|                     Object.assign(pageParams, params); | ||||
|                 } | ||||
|                 navCtrl.push('AddonModAssignIndexPage', pageParams, options); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @ -92,19 +92,19 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
| 
 | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         return this.assignProvider.getAssignment(courseId, module.id, siteId).then((assign) => { | ||||
|         return this.assignProvider.getAssignment(courseId, module.id, false, siteId).then((assign) => { | ||||
|             // Get intro files and attachments.
 | ||||
|             let files = assign.introattachments || []; | ||||
|             files = files.concat(this.getIntroFilesFromInstance(module, assign)); | ||||
| 
 | ||||
|             // Now get the files in the submissions.
 | ||||
|             return this.assignProvider.getSubmissions(assign.id, siteId).then((data) => { | ||||
|             return this.assignProvider.getSubmissions(assign.id, false, siteId).then((data) => { | ||||
|                 const blindMarking = assign.blindmarking && !assign.revealidentities; | ||||
| 
 | ||||
|                 if (data.canviewsubmissions) { | ||||
|                     // Teacher, get all submissions.
 | ||||
|                     return this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id, blindMarking, | ||||
|                             undefined, siteId).then((submissions) => { | ||||
|                             undefined, false, siteId).then((submissions) => { | ||||
| 
 | ||||
|                         const promises = []; | ||||
| 
 | ||||
| @ -156,7 +156,8 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
|     protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string) | ||||
|             : Promise<any[]> { | ||||
| 
 | ||||
|         return this.assignProvider.getSubmissionStatus(assign.id, submitId, blindMarking, true, false, siteId).then((response) => { | ||||
|         return this.assignProvider.getSubmissionStatusWithRetry(assign, submitId, undefined, blindMarking, true, false, siteId) | ||||
|                 .then((response) => { | ||||
|             const promises = []; | ||||
| 
 | ||||
|             if (response.lastattempt) { | ||||
| @ -200,6 +201,17 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
|         return this.assignProvider.invalidateContent(moduleId, courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate WS calls needed to determine module status. | ||||
|      * | ||||
|      * @param  {any}    module   Module. | ||||
|      * @param  {number} courseId Course ID the module belongs to. | ||||
|      * @return {Promise<any>} Promise resolved when invalidated. | ||||
|      */ | ||||
|     invalidateModule(module: any, courseId: number): Promise<any> { | ||||
|         return this.assignProvider.invalidateAssignmentData(courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
| @ -238,12 +250,12 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Get assignment to retrieve all its submissions.
 | ||||
|         promises.push(this.assignProvider.getAssignment(courseId, module.id, siteId).then((assign) => { | ||||
|         promises.push(this.assignProvider.getAssignment(courseId, module.id, true, siteId).then((assign) => { | ||||
|             const subPromises = [], | ||||
|                 blindMarking = assign.blindmarking && !assign.revealidentities; | ||||
| 
 | ||||
|             if (blindMarking) { | ||||
|                 subPromises.push(this.assignProvider.getAssignmentUserMappings(assign.id, undefined, siteId).catch(() => { | ||||
|                 subPromises.push(this.assignProvider.getAssignmentUserMappings(assign.id, undefined, true, siteId).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 })); | ||||
|             } | ||||
| @ -252,10 +264,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
| 
 | ||||
|             subPromises.push(this.courseHelper.getModuleCourseIdByInstance(assign.id, 'assign', siteId)); | ||||
| 
 | ||||
|             // Get all files and fetch them.
 | ||||
|             subPromises.push(this.getFiles(module, courseId, single, siteId).then((files) => { | ||||
|                 return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); | ||||
|             })); | ||||
|             // Download intro files and attachments. Do not call getFiles because it'd call some WS twice.
 | ||||
|             let files = assign.introattachments || []; | ||||
|                 files = files.concat(this.getIntroFilesFromInstance(module, assign)); | ||||
| 
 | ||||
|             subPromises.push(this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id)); | ||||
| 
 | ||||
|             return Promise.all(subPromises); | ||||
|         })); | ||||
| @ -274,63 +287,74 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
|      * @return {Promise<any>} Promise resolved when prefetched, rejected otherwise. | ||||
|      */ | ||||
|     protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise<any> { | ||||
| 
 | ||||
|         // Get submissions.
 | ||||
|         return this.assignProvider.getSubmissions(assign.id, siteId).then((data) => { | ||||
|         return this.assignProvider.getSubmissions(assign.id, true, siteId).then((data) => { | ||||
|             const promises = [], | ||||
|                 blindMarking = assign.blindmarking && !assign.revealidentities; | ||||
| 
 | ||||
|             if (data.canviewsubmissions) { | ||||
|                 // Teacher. Do not send participants to getSubmissionsUserData to retrieve user profiles.
 | ||||
|                 promises.push(this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id, blindMarking, | ||||
|                         undefined, siteId).then((submissions) => { | ||||
|                 promises.push(this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((groupInfo) => { | ||||
|                     const groupProms = []; | ||||
|                     if (!groupInfo.groups || groupInfo.groups.length == 0) { | ||||
|                         groupInfo.groups = [{id: 0}]; | ||||
|                     } | ||||
| 
 | ||||
|                     const subPromises = []; | ||||
|                     groupInfo.groups.forEach((group) => { | ||||
|                         groupProms.push(this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id, | ||||
|                                 blindMarking, undefined, true, siteId).then((submissions) => { | ||||
| 
 | ||||
|                     submissions.forEach((submission) => { | ||||
|                         subPromises.push(this.assignProvider.getSubmissionStatus(assign.id, submission.submitid, | ||||
|                                 !!submission.blindid, true, false, siteId).then((subm) => { | ||||
|                             return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId); | ||||
|                         }).catch((error) => { | ||||
|                             if (error && error.errorcode == 'nopermission') { | ||||
|                                 // The user does not have persmission to view this submission, ignore it.
 | ||||
|                                 return Promise.resolve(); | ||||
|                             const subPromises = []; | ||||
| 
 | ||||
|                             submissions.forEach((submission) => { | ||||
|                                 subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submission.submitid, | ||||
|                                         group.id, !!submission.blindid, true, true, siteId).then((subm) => { | ||||
|                                     return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId); | ||||
|                                 }).catch((error) => { | ||||
|                                     if (error && error.errorcode == 'nopermission') { | ||||
|                                         // The user does not have persmission to view this submission, ignore it.
 | ||||
|                                         return Promise.resolve(); | ||||
|                                     } | ||||
| 
 | ||||
|                                     return Promise.reject(error); | ||||
|                                 })); | ||||
|                             }); | ||||
| 
 | ||||
|                             if (!assign.markingworkflow) { | ||||
|                                 // Get assignment grades only if workflow is not enabled to check grading date.
 | ||||
|                                 subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, true, siteId)); | ||||
|                             } | ||||
| 
 | ||||
|                             return Promise.reject(error); | ||||
|                             // Prefetch the submission of the current user even if it does not exist, this will be create it.
 | ||||
|                             if (!data.submissions || !data.submissions.find((subm) => subm.submitid == userId)) { | ||||
|                                 subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, userId, group.id, | ||||
|                                         false, true, true, siteId).then((subm) => { | ||||
|                                     return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); | ||||
|                                 })); | ||||
|                             } | ||||
| 
 | ||||
|                             return Promise.all(subPromises); | ||||
|                         })); | ||||
| 
 | ||||
|                         // Get list participants.
 | ||||
|                         groupProms.push(this.assignHelper.getParticipants(assign, group.id, true, siteId).then((participants) => { | ||||
|                             participants.forEach((participant) => { | ||||
|                                 if (participant.profileimageurl) { | ||||
|                                     this.filepoolProvider.addToQueueByUrl(siteId, participant.profileimageurl); | ||||
|                                 } | ||||
|                             }); | ||||
|                         }).catch(() => { | ||||
|                             // Fail silently (Moodle < 3.2).
 | ||||
|                         })); | ||||
|                     }); | ||||
| 
 | ||||
|                     if (!assign.markingworkflow) { | ||||
|                         // Get assignment grades only if workflow is not enabled to check grading date.
 | ||||
|                         subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, siteId)); | ||||
|                     } | ||||
| 
 | ||||
|                     // Prefetch the submission of the current user even if it does not exist, this will be create it.
 | ||||
|                     if (!data.submissions || !data.submissions.find((subm) => subm.submitid == userId)) { | ||||
|                         subPromises.push(this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, false, siteId) | ||||
|                                 .then((subm) => { | ||||
|                             return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); | ||||
|                         })); | ||||
|                     } | ||||
| 
 | ||||
|                     return Promise.all(subPromises); | ||||
|                 })); | ||||
| 
 | ||||
|                 // Get list participants.
 | ||||
|                 promises.push(this.assignHelper.getParticipants(assign, siteId).then((participants) => { | ||||
|                     participants.forEach((participant) => { | ||||
|                         if (participant.profileimageurl) { | ||||
|                             this.filepoolProvider.addToQueueByUrl(siteId, participant.profileimageurl); | ||||
|                         } | ||||
|                     }); | ||||
|                 }).catch(() => { | ||||
|                     // Fail silently (Moodle < 3.2).
 | ||||
|                     return Promise.all(groupProms); | ||||
|                 })); | ||||
|             } else { | ||||
|                 // Student.
 | ||||
|                 promises.push( | ||||
|                     this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, false, siteId).then((subm) => { | ||||
|                     this.assignProvider.getSubmissionStatusWithRetry(assign, userId, undefined, false, true, true, siteId) | ||||
|                             .then((subm) => { | ||||
|                         return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); | ||||
|                     }).catch((error) => { | ||||
|                         // Ignore if the user can't view their own submission.
 | ||||
| @ -341,8 +365,8 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             promises.push(this.groupsProvider.activityHasGroups(assign.cmid)); | ||||
|             promises.push(this.groupsProvider.getActivityAllowedGroups(assign.cmid, undefined, siteId)); | ||||
|             promises.push(this.groupsProvider.activityHasGroups(assign.cmid, siteId, true)); | ||||
|             promises.push(this.groupsProvider.getActivityAllowedGroups(assign.cmid, undefined, siteId, true)); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }); | ||||
| @ -378,7 +402,16 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
|                 // Prefetch submission plugins data.
 | ||||
|                 if (userSubmission.plugins) { | ||||
|                     userSubmission.plugins.forEach((plugin) => { | ||||
|                         // Prefetch the plugin WS data.
 | ||||
|                         promises.push(this.submissionDelegate.prefetch(assign, userSubmission, plugin, siteId)); | ||||
| 
 | ||||
|                         // Prefetch the plugin files.
 | ||||
|                         promises.push(this.submissionDelegate.getPluginFiles(assign, userSubmission, plugin, siteId) | ||||
|                                 .then((files) => { | ||||
|                             return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); | ||||
|                         }).catch(() => { | ||||
|                             // Ignore errors.
 | ||||
|                         })); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
| @ -392,18 +425,26 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan | ||||
|         // Prefetch feedback.
 | ||||
|         if (submission.feedback) { | ||||
|             // Get profile and image of the grader.
 | ||||
|             if (submission.feedback.grade && submission.feedback.grade.grader) { | ||||
|             if (submission.feedback.grade && submission.feedback.grade.grader > 0) { | ||||
|                 userIds.push(submission.feedback.grade.grader); | ||||
|             } | ||||
| 
 | ||||
|             if (userId) { | ||||
|                 promises.push(this.gradesHelper.getGradeModuleItems(courseId, moduleId, userId, undefined, siteId)); | ||||
|                 promises.push(this.gradesHelper.getGradeModuleItems(courseId, moduleId, userId, undefined, siteId, true)); | ||||
|             } | ||||
| 
 | ||||
|             // Prefetch feedback plugins data.
 | ||||
|             if (submission.feedback.plugins) { | ||||
|                 submission.feedback.plugins.forEach((plugin) => { | ||||
|                     // Prefetch the plugin WS data.
 | ||||
|                     promises.push(this.feedbackDelegate.prefetch(assign, submission, plugin, siteId)); | ||||
| 
 | ||||
|                     // Prefetch the plugin files.
 | ||||
|                     promises.push(this.feedbackDelegate.getPluginFiles(assign, submission, plugin, siteId).then((files) => { | ||||
|                         return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); | ||||
|                     }).catch(() => { | ||||
|                         // Ignore errors.
 | ||||
|                     })); | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| { | ||||
|     "pluginname": "Online text submissions" | ||||
|     "pluginname": "Online text submissions", | ||||
|     "wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again." | ||||
| } | ||||
| @ -14,6 +14,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable, Injector } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreWSProvider } from '@providers/ws'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| @ -31,7 +32,7 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign | ||||
|     name = 'AddonModAssignSubmissionOnlineTextHandler'; | ||||
|     type = 'onlinetext'; | ||||
| 
 | ||||
|     constructor(private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, | ||||
|     constructor(private translate: TranslateService, private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, | ||||
|         private textUtils: CoreTextUtilsProvider, private assignProvider: AddonModAssignProvider, | ||||
|         private assignOfflineProvider: AddonModAssignOfflineProvider, private assignHelper: AddonModAssignHelperProvider) { } | ||||
| 
 | ||||
| @ -238,6 +239,19 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign | ||||
| 
 | ||||
|         let text = this.getTextToSubmit(plugin, inputData); | ||||
| 
 | ||||
|         // Check word limit.
 | ||||
|         const configs = this.assignHelper.getPluginConfig(assign, 'assignsubmission', plugin.type); | ||||
|         if (parseInt(configs.wordlimitenabled, 10)) { | ||||
|             const words = this.textUtils.countWords(text); | ||||
|             const wordlimit = parseInt(configs.wordlimit, 10); | ||||
|             if (words > wordlimit) { | ||||
|                 const params = {$a: {count: words, limit: wordlimit}}; | ||||
|                 const message = this.translate.instant('addon.mod_assign_submission_onlinetext.wordlimitexceeded', params); | ||||
| 
 | ||||
|                 return Promise.reject(message); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Add some HTML to the text if needed.
 | ||||
|         text = this.textUtils.formatHtmlLines(text); | ||||
| 
 | ||||
|  | ||||
| @ -20,12 +20,10 @@ import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CoreCourseComponentsModule } from '@core/course/components/components.module'; | ||||
| import { AddonModBookIndexComponent } from './index/index'; | ||||
| import { AddonModBookTocPopoverComponent } from './toc-popover/toc-popover'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModBookIndexComponent, | ||||
|         AddonModBookTocPopoverComponent | ||||
|         AddonModBookIndexComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
| @ -38,12 +36,10 @@ import { AddonModBookTocPopoverComponent } from './toc-popover/toc-popover'; | ||||
|     providers: [ | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonModBookIndexComponent, | ||||
|         AddonModBookTocPopoverComponent | ||||
|         AddonModBookIndexComponent | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonModBookIndexComponent, | ||||
|         AddonModBookTocPopoverComponent | ||||
|         AddonModBookIndexComponent | ||||
|     ] | ||||
| }) | ||||
| export class AddonModBookComponentsModule {} | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons end> | ||||
|     <button ion-button icon-only (click)="showToc($event)"> | ||||
|     <button ion-button icon-only (click)="showToc($event)" [attr.aria-label]="'addon.mod_book.toc' | translate" aria-haspopup="true" *ngIf="loaded"> | ||||
|         <ion-icon name="bookmark"></ion-icon> | ||||
|     </button> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></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 *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item> | ||||
|  | ||||
| @ -12,14 +12,13 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Optional, Injector } from '@angular/core'; | ||||
| import { Content, PopoverController } from 'ionic-angular'; | ||||
| import { Component, Optional, Injector, Input } from '@angular/core'; | ||||
| import { Content, ModalController } from 'ionic-angular'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; | ||||
| import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book'; | ||||
| import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler'; | ||||
| import { AddonModBookTocPopoverComponent } from '../../components/toc-popover/toc-popover'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays a book. | ||||
| @ -29,6 +28,8 @@ import { AddonModBookTocPopoverComponent } from '../../components/toc-popover/to | ||||
|     templateUrl: 'addon-mod-book-index.html', | ||||
| }) | ||||
| export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent { | ||||
|     @Input() initialChapterId: string; // The initial chapter ID to load.
 | ||||
| 
 | ||||
|     component = AddonModBookProvider.COMPONENT; | ||||
|     chapterContent: string; | ||||
|     previousChapter: string; | ||||
| @ -40,7 +41,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | ||||
| 
 | ||||
|     constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider, | ||||
|             private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler, | ||||
|             private popoverCtrl: PopoverController, @Optional() private content: Content) { | ||||
|             private modalCtrl: ModalController, @Optional() private content: Content) { | ||||
|         super(injector); | ||||
|     } | ||||
| 
 | ||||
| @ -59,15 +60,23 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | ||||
|      * @param {MouseEvent} event Event. | ||||
|      */ | ||||
|     showToc(event: MouseEvent): void { | ||||
|         const popover = this.popoverCtrl.create(AddonModBookTocPopoverComponent, { | ||||
|             chapters: this.chapters | ||||
|         // Create the toc modal.
 | ||||
|         const modal =  this.modalCtrl.create('AddonModBookTocPage', { | ||||
|             chapters: this.chapters, | ||||
|             selected: this.currentChapter | ||||
|         }, { cssClass: 'core-modal-lateral', | ||||
|             showBackdrop: true, | ||||
|             enableBackdropDismiss: true, | ||||
|             enterAnimation: 'core-modal-lateral-transition', | ||||
|             leaveAnimation: 'core-modal-lateral-transition' }); | ||||
| 
 | ||||
|         modal.onDidDismiss((chapterId) => { | ||||
|             if (chapterId) { | ||||
|                 this.changeChapter(chapterId); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         popover.onDidDismiss((chapterId) => { | ||||
|             this.changeChapter(chapterId); | ||||
|         }); | ||||
| 
 | ||||
|         popover.present({ | ||||
|         modal.present({ | ||||
|             ev: event | ||||
|         }); | ||||
|     } | ||||
| @ -128,7 +137,19 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | ||||
|             this.contentsMap = this.bookProvider.getContentsMap(this.module.contents); | ||||
|             this.chapters = this.bookProvider.getTocList(this.module.contents); | ||||
| 
 | ||||
|             if (typeof this.currentChapter == 'undefined' && typeof this.initialChapterId != 'undefined' && this.chapters) { | ||||
|                 // Initial chapter set. Validate that the chapter exists.
 | ||||
|                 const chapter = this.chapters.find((chapter) => { | ||||
|                     return chapter.id == this.initialChapterId; | ||||
|                 }); | ||||
| 
 | ||||
|                 if (chapter) { | ||||
|                     this.currentChapter = this.initialChapterId; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (typeof this.currentChapter == 'undefined') { | ||||
|                 // Load the first chapter.
 | ||||
|                 this.currentChapter = this.bookProvider.getFirstChapter(this.chapters); | ||||
|             } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +0,0 @@ | ||||
| <ion-list> | ||||
|     <a ion-item text-wrap *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)" detail-none> | ||||
|         <p [attr.padding-left]="chapter.level == 1 ? true : null">{{chapter.title}}</p> | ||||
|     </a> | ||||
| </ion-list> | ||||
| @ -1,4 +1,5 @@ | ||||
| { | ||||
|     "errorchapter": "Error reading chapter of book.", | ||||
|     "modulenameplural": "Books" | ||||
|     "modulenameplural": "Books", | ||||
|     "toc": "Table of contents" | ||||
| } | ||||
| @ -12,5 +12,5 @@ | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
| 
 | ||||
|     <addon-mod-book-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-book-index> | ||||
|     <addon-mod-book-index [module]="module" [courseId]="courseId" [initialChapterId]="chapterId" (dataRetrieved)="updateData($event)"></addon-mod-book-index> | ||||
| </ion-content> | ||||
|  | ||||
| @ -30,10 +30,12 @@ export class AddonModBookIndexPage { | ||||
|     title: string; | ||||
|     module: any; | ||||
|     courseId: number; | ||||
|     chapterId: number; | ||||
| 
 | ||||
|     constructor(navParams: NavParams) { | ||||
|         this.module = navParams.get('module') || {}; | ||||
|         this.courseId = navParams.get('courseId'); | ||||
|         this.chapterId = navParams.get('chapterId'); | ||||
|         this.title = this.module.name; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										19
									
								
								src/addon/mod/book/pages/toc/toc.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/addon/mod/book/pages/toc/toc.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title>{{ 'addon.mod_book.toc' | translate }}</ion-title> | ||||
|         <ion-buttons end> | ||||
|             <button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon name="close"></ion-icon> | ||||
|             </button> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <nav> | ||||
|         <ion-list> | ||||
|             <a ion-item text-wrap *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)" [class.core-nav-item-selected]="selected == chapter.id"> | ||||
|                 <p [attr.padding-left]="chapter.level == 1 ? true : null">{{chapter.title}}</p> | ||||
|             </a> | ||||
|         </ion-list> | ||||
|     </nav> | ||||
| </ion-content> | ||||
							
								
								
									
										31
									
								
								src/addon/mod/book/pages/toc/toc.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/addon/mod/book/pages/toc/toc.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { IonicPageModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { AddonModBookTocPage } from './toc'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModBookTocPage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreDirectivesModule, | ||||
|         IonicPageModule.forChild(AddonModBookTocPage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class AddonModBookTocPageModule {} | ||||
| @ -13,21 +13,24 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component } from '@angular/core'; | ||||
| import { NavParams, ViewController } from 'ionic-angular'; | ||||
| import { IonicPage, NavParams, ViewController } from 'ionic-angular'; | ||||
| import { AddonModBookTocChapter } from '../../providers/book'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to display the TOC of a book. | ||||
|  * Modal to display the TOC of a book. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-book-toc-modal' }) | ||||
| @Component({ | ||||
|     selector: 'addon-mod-book-toc-popover', | ||||
|     templateUrl: 'addon-mod-assign-submission-toc-popover.html' | ||||
|     selector: 'page-addon-mod-book-toc', | ||||
|     templateUrl: 'toc.html' | ||||
| }) | ||||
| export class AddonModBookTocPopoverComponent { | ||||
| export class AddonModBookTocPage { | ||||
|     chapters: AddonModBookTocChapter[]; | ||||
|     selected: number; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, private viewCtrl: ViewController) { | ||||
|         this.chapters = navParams.get('chapters') || []; | ||||
|         this.selected = navParams.get('selected'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -38,4 +41,11 @@ export class AddonModBookTocPopoverComponent { | ||||
|     loadChapter(id: string): void { | ||||
|         this.viewCtrl.dismiss(id); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         this.viewCtrl.dismiss(); | ||||
|     } | ||||
| } | ||||
| @ -22,6 +22,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||
| 
 | ||||
| /** | ||||
|  * A book chapter inside the toc list. | ||||
| @ -64,7 +65,8 @@ export class AddonModBookProvider { | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, | ||||
|             private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, private http: Http, | ||||
|             private utils: CoreUtilsProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) { | ||||
|             private utils: CoreUtilsProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, | ||||
|             private logHelper: CoreCourseLogHelperProvider) { | ||||
|         this.logger = logger.getInstance('AddonModBookProvider'); | ||||
|     } | ||||
| 
 | ||||
| @ -378,14 +380,15 @@ export class AddonModBookProvider { | ||||
|      * | ||||
|      * @param {number} id Module ID. | ||||
|      * @param {string} chapterId Chapter ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when the WS call is successful. | ||||
|      */ | ||||
|     logView(id: number, chapterId: string): Promise<any> { | ||||
|     logView(id: number, chapterId: string, siteId?: string): Promise<any> { | ||||
|         const params = { | ||||
|             bookid: id, | ||||
|             chapterid: chapterId | ||||
|         }; | ||||
| 
 | ||||
|         return this.sitesProvider.getCurrentSite().write('mod_book_view_book', params); | ||||
|         return this.logHelper.log('mod_book_view_book', params, AddonModBookProvider.COMPONENT, id, siteId); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -15,6 +15,7 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; | ||||
| import { CoreCourseHelperProvider } from '@core/course/providers/helper'; | ||||
| import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to book. | ||||
| @ -26,4 +27,27 @@ export class AddonModBookLinkHandler extends CoreContentLinksModuleIndexHandler | ||||
|     constructor(courseHelper: CoreCourseHelperProvider) { | ||||
|         super(courseHelper, 'AddonModBook', 'book'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the list of actions for a link (url). | ||||
|      * | ||||
|      * @param {string[]} siteIds List of sites the URL belongs to. | ||||
|      * @param {string} url The URL to treat. | ||||
|      * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} | ||||
|      * @param {number} [courseId] Course ID related to the URL. Optional but recommended. | ||||
|      * @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions. | ||||
|      */ | ||||
|     getActions(siteIds: string[], url: string, params: any, courseId?: number): | ||||
|             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { | ||||
| 
 | ||||
|         const modParams = params.chapterid ? {chapterId: params.chapterid} : undefined; | ||||
|         courseId = courseId || params.courseid || params.cid; | ||||
| 
 | ||||
|         return [{ | ||||
|             action: (siteId, navCtrl?): void => { | ||||
|                 this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined, | ||||
|                     this.useModNameToGetModule ? this.modName : undefined, modParams); | ||||
|             } | ||||
|         }]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -65,8 +65,12 @@ export class AddonModBookModuleHandler implements CoreCourseModuleHandler { | ||||
|             title: module.name, | ||||
|             class: 'addon-mod_book-handler', | ||||
|             showDownloadButton: true, | ||||
|             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { | ||||
|                 navCtrl.push('AddonModBookIndexPage', {module: module, courseId: courseId}, options); | ||||
|             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void { | ||||
|                 const pageParams = {module: module, courseId: courseId}; | ||||
|                 if (params) { | ||||
|                     Object.assign(pageParams, params); | ||||
|                 } | ||||
|                 navCtrl.push('AddonModBookIndexPage', pageParams, options); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @ -15,11 +15,13 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; | ||||
| import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; | ||||
| import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; | ||||
| import { AddonModChatComponentsModule } from './components/components.module'; | ||||
| import { AddonModChatProvider } from './providers/chat'; | ||||
| import { AddonModChatLinkHandler } from './providers/link-handler'; | ||||
| import { AddonModChatListLinkHandler } from './providers/list-link-handler'; | ||||
| import { AddonModChatModuleHandler } from './providers/module-handler'; | ||||
| import { AddonModChatPrefetchHandler } from './providers/prefetch-handler'; | ||||
| 
 | ||||
| // List of providers (without handlers).
 | ||||
| export const ADDON_MOD_CHAT_PROVIDERS: any[] = [ | ||||
| @ -37,15 +39,18 @@ export const ADDON_MOD_CHAT_PROVIDERS: any[] = [ | ||||
|         AddonModChatLinkHandler, | ||||
|         AddonModChatListLinkHandler, | ||||
|         AddonModChatModuleHandler, | ||||
|         AddonModChatPrefetchHandler | ||||
|     ] | ||||
| }) | ||||
| export class AddonModChatModule { | ||||
|     constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChatModuleHandler, | ||||
|             contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModChatLinkHandler, | ||||
|             prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModChatPrefetchHandler, | ||||
|             listLinkHandler: AddonModChatListLinkHandler) { | ||||
| 
 | ||||
|         moduleDelegate.registerHandler(moduleHandler); | ||||
|         contentLinksDelegate.registerHandler(linkHandler); | ||||
|         contentLinksDelegate.registerHandler(listLinkHandler); | ||||
|         prefetchDelegate.registerHandler(prefetchHandler); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,7 +3,9 @@ | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></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="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="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| @ -16,7 +18,8 @@ | ||||
|         <ion-icon name="time"></ion-icon> {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }} | ||||
|     </ion-card> | ||||
| 
 | ||||
|     <div padding-horizontal> | ||||
|     <div padding> | ||||
|         <a ion-button block color="primary" (click)="enterChat()">{{ 'addon.mod_chat.enterchat' | translate }}</a> | ||||
|         <a ion-button block color="light" margin-top *ngIf="sessionsAvailable" (click)="viewSessions()">{{ 'addon.mod_chat.viewreport' | translate }}</a> | ||||
|     </div> | ||||
| </core-loading> | ||||
|  | ||||
| @ -33,9 +33,10 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     chatInfo: any; | ||||
| 
 | ||||
|     protected title: string; | ||||
|     protected sessionsAvailable = false; | ||||
| 
 | ||||
|     constructor(injector: Injector, private chatProvider: AddonModChatProvider, private timeUtils: CoreTimeUtilsProvider, | ||||
|             private navCtrl: NavController) { | ||||
|             protected navCtrl: NavController) { | ||||
|         super(injector); | ||||
|     } | ||||
| 
 | ||||
| @ -83,6 +84,10 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp | ||||
| 
 | ||||
|             // All data obtained, now fill the context menu.
 | ||||
|             this.fillContextMenu(refresh); | ||||
| 
 | ||||
|             return this.chatProvider.areSessionsAvailable().then((available) => { | ||||
|                 this.sessionsAvailable = available; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -93,4 +98,11 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|         const title = this.chat.name || this.moduleName; | ||||
|         this.navCtrl.push('AddonModChatChatPage', {chatId: this.chat.id, courseId: this.courseId, title: title }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View past sessions. | ||||
|      */ | ||||
|     viewSessions(): void { | ||||
|         this.navCtrl.push('AddonModChatSessionsPage', {courseId: this.courseId, chatId: this.chat.id, cmId: this.module.id}); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| { | ||||
|     "beep": "Beep", | ||||
|     "chatreport": "Chat sessions", | ||||
|     "currentusers": "Current users", | ||||
|     "enterchat": "Click here to enter the chat now", | ||||
|     "entermessage": "Enter your message", | ||||
| @ -11,10 +12,14 @@ | ||||
|     "messagebeepsyou": "{{$a}} has just beeped you!", | ||||
|     "messageenter": "{{$a}} has just entered this chat", | ||||
|     "messageexit": "{{$a}} has left this chat", | ||||
|     "messages": "Messages", | ||||
|     "modulenameplural": "Chats", | ||||
|     "mustbeonlinetosendmessages": "You must be online to send messages.", | ||||
|     "nomessages": "No messages yet", | ||||
|     "nosessionsfound": "No sessions found", | ||||
|     "send": "Send", | ||||
|     "sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)", | ||||
|     "talk": "Talk" | ||||
|     "showincompletesessions": "Show incomplete sessions", | ||||
|     "talk": "Talk", | ||||
|     "viewreport": "View past chat sessions" | ||||
| } | ||||
| @ -0,0 +1,40 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title>{{ 'addon.mod_chat.messages' | translate }}</ion-title> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher [enabled]="loaded" (ionRefresh)="refreshMessages($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <div *ngFor="let message of messages; index as index; last as last"> | ||||
|             <div text-center *ngIf="showDate(messages[index], messages[index - 1])" class="addon-mod-chat-notice"> | ||||
|                 <ion-badge text-wrap color="light"> | ||||
|                     <span>{{ message.timestamp * 1000 | coreFormatDate:"strftimedayshort" }}</span> | ||||
|                 </ion-badge> | ||||
|             </div> | ||||
| 
 | ||||
|             <div text-center *ngIf="message.issystem && message.message == 'enter'" class="addon-mod-chat-notice"> | ||||
|                 <ion-badge text-wrap color="light"> | ||||
|                     <span>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }}</span> | ||||
|                 </ion-badge> | ||||
|             </div> | ||||
| 
 | ||||
|             <div text-center *ngIf="message.issystem && message.message == 'exit'" class="addon-mod-chat-notice"> | ||||
|                 <ion-badge text-wrap color="light"> | ||||
|                     <span>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }}</span> | ||||
|                 </ion-badge> | ||||
|             </div> | ||||
| 
 | ||||
|             <ion-item text-wrap *ngIf="!message.issystem && message.message.substr(0, 4) != 'beep'" class="addon-mod-chat-message"> | ||||
|                 <ion-avatar core-user-avatar [user]="message" item-start></ion-avatar> | ||||
|                 <h2> | ||||
|                     <p float-end>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }}</p> | ||||
|                     <core-format-text [text]="message.userfullname"></core-format-text> | ||||
|                 </h2> | ||||
|                 <core-format-text [text]="message.message"></core-format-text> | ||||
|             </ion-item> | ||||
|         </div> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
| @ -0,0 +1,37 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { IonicPageModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CorePipesModule } from '@pipes/pipes.module'; | ||||
| import { AddonModChatComponentsModule } from '../../components/components.module'; | ||||
| import { AddonModChatSessionMessagesPage } from './session-messages'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModChatSessionMessagesPage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CorePipesModule, | ||||
|         AddonModChatComponentsModule, | ||||
|         IonicPageModule.forChild(AddonModChatSessionMessagesPage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class AddonModChatSessionMessagesPageModule {} | ||||
| @ -0,0 +1,9 @@ | ||||
| ion-app.app-root page-addon-mod-chat-session-messages { | ||||
|     .addon-mod-chat-notice { | ||||
|         margin-top: 10px; | ||||
|         margin-bottom: 10px; | ||||
|     } | ||||
|     .addon-mod-chat-message { | ||||
|         align-items: flex-start; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,95 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component } from '@angular/core'; | ||||
| import { IonicPage, NavParams } from 'ionic-angular'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { AddonModChatProvider } from '../../providers/chat'; | ||||
| import * as moment from 'moment'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays list of chat session messages. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-chat-session-messages' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-chat-session-messages', | ||||
|     templateUrl: 'session-messages.html', | ||||
| }) | ||||
| export class AddonModChatSessionMessagesPage { | ||||
| 
 | ||||
|     protected courseId: number; | ||||
|     protected chatId: number; | ||||
|     protected sessionStart: number; | ||||
|     protected sessionEnd: number; | ||||
|     protected groupId: number; | ||||
|     protected loaded = false; | ||||
|     protected messages = []; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private chatProvider: AddonModChatProvider) { | ||||
|         this.courseId = navParams.get('courseId'); | ||||
|         this.chatId = navParams.get('chatId'); | ||||
|         this.groupId = navParams.get('groupId'); | ||||
|         this.sessionStart = navParams.get('sessionStart'); | ||||
|         this.sessionEnd = navParams.get('sessionEnd'); | ||||
| 
 | ||||
|         this.fetchMessages(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch session messages. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchMessages(): Promise<any> { | ||||
|         return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId) | ||||
|                 .then((messages) => { | ||||
|             return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => { | ||||
|                 this.messages = messages; | ||||
|             }); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh session messages. | ||||
|      * | ||||
|      * @param {any} refresher Refresher. | ||||
|      */ | ||||
|     refreshMessages(refresher: any): void { | ||||
|         this.chatProvider.invalidateSessionMessages(this.chatId, this.sessionStart, this.groupId).finally(() => { | ||||
|             this.fetchMessages().finally(() => { | ||||
|                 refresher.complete(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|    /** | ||||
|     * Check if the date should be displayed between messages (when the day changes at midnight for example). | ||||
|     * | ||||
|     * @param  {any} message     New message object. | ||||
|     * @param  {any} prevMessage Previous message object. | ||||
|     * @return {boolean} True if messages are from diferent days, false othetwise. | ||||
|     */ | ||||
|    showDate(message: any, prevMessage: any): boolean { | ||||
|        if (!prevMessage) { | ||||
|            return true; | ||||
|        } | ||||
| 
 | ||||
|        // Check if day has changed.
 | ||||
|        return !moment(message.timestamp * 1000).isSame(prevMessage.timestamp * 1000, 'day'); | ||||
|    } | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/addon/mod/chat/pages/sessions/sessions.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/addon/mod/chat/pages/sessions/sessions.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title>{{ 'addon.mod_chat.chatreport' | translate }}</ion-title> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <core-split-view> | ||||
|     <ion-content> | ||||
|         <ion-refresher [enabled]="loaded" (ionRefresh)="refreshSessions($event)"> | ||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|         </ion-refresher> | ||||
|         <core-loading [hideUntil]="loaded"> | ||||
|             <ion-item text-wrap *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)"> | ||||
|                 <ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label> | ||||
|                 <ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label> | ||||
|                 <ion-select [(ngModel)]="groupId" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-groupslabel" interface="action-sheet"> | ||||
|                     <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option> | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
|             <ion-item> | ||||
|                 <ion-label id="addon-chat-showalllabel">{{ 'addon.mod_chat.showincompletesessions' | translate }}</ion-label> | ||||
|                 <ion-toggle [(ngModel)]="showAll" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-showalllabel"></ion-toggle> | ||||
|             </ion-item> | ||||
|             <ion-card *ngFor="let session of sessions" (click)="openSession(session)" | ||||
|                     [class.addon-mod-chat-session-selected]="session.sessionstart == selectedSessionStart && groupId == selectedSessionGroupId" | ||||
|                     [class.addon-mod-chat-session-show-more]="session.sessionusers.length < session.allsessionusers.length"> | ||||
|                 <ion-item text-wrap> | ||||
|                     <h2>{{ session.sessionstart * 1000 | coreFormatDate }}</h2> | ||||
|                     <p *ngIf="session.duration">{{ session.duration | coreDuration }}</p> | ||||
|                 </ion-item> | ||||
|                 <ion-card-content> | ||||
|                     <p *ngFor="let user of session.sessionusers"> | ||||
|                         {{ user.userfullname }} <span *ngIf="user.messagecount">({{ user.messagecount }})</span> | ||||
|                     </p> | ||||
|                 </ion-card-content> | ||||
|                 <div *ngIf="session.sessionusers.length < session.allsessionusers.length"> | ||||
|                     <button ion-button clear (click)="showMoreUsers(session, $event)"> | ||||
|                         {{ 'core.showmore' | translate }} | ||||
|                     </button> | ||||
|                 </div> | ||||
|             </ion-card> | ||||
|             <core-empty-box *ngIf="sessions.length == 0" icon="chatbubbles" [message]="'addon.mod_chat.nosessionsfound' | translate"> | ||||
|             </core-empty-box> | ||||
|         </core-loading> | ||||
|     </ion-content> | ||||
| </core-split-view> | ||||
							
								
								
									
										37
									
								
								src/addon/mod/chat/pages/sessions/sessions.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/addon/mod/chat/pages/sessions/sessions.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { IonicPageModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CorePipesModule } from '@pipes/pipes.module'; | ||||
| import { AddonModChatComponentsModule } from '../../components/components.module'; | ||||
| import { AddonModChatSessionsPage } from './sessions'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonModChatSessionsPage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CorePipesModule, | ||||
|         AddonModChatComponentsModule, | ||||
|         IonicPageModule.forChild(AddonModChatSessionsPage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class AddonModChatSessionsPageModule {} | ||||
							
								
								
									
										8
									
								
								src/addon/mod/chat/pages/sessions/sessions.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/addon/mod/chat/pages/sessions/sessions.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| ion-app.app-root page-addon-mod-chat-sessions { | ||||
|     .addon-mod-chat-session-show-more .card-content{ | ||||
|         padding-bottom: 0; | ||||
|     } | ||||
|     .addon-mod-chat-session-selected { | ||||
|         border-top: 5px solid $core-splitview-selected; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										165
									
								
								src/addon/mod/chat/pages/sessions/sessions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								src/addon/mod/chat/pages/sessions/sessions.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, ViewChild } from '@angular/core'; | ||||
| import { IonicPage, NavParams } from 'ionic-angular'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { AddonModChatProvider } from '../../providers/chat'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays list of chat sessions. | ||||
|  */ | ||||
| @IonicPage({ segment: 'addon-mod-chat-sessions' }) | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-chat-sessions', | ||||
|     templateUrl: 'sessions.html', | ||||
| }) | ||||
| export class AddonModChatSessionsPage { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; | ||||
| 
 | ||||
|     protected courseId: number; | ||||
|     protected cmId: number; | ||||
|     protected chatId: number; | ||||
|     protected loaded = false; | ||||
|     protected showAll = false; | ||||
|     protected groupId = 0; | ||||
|     protected groupInfo: CoreGroupInfo; | ||||
|     protected sessions = []; | ||||
|     protected selectedSessionStart: number; | ||||
|     protected selectedSessionGroupId: number; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, private chatProvider: AddonModChatProvider, private domUtils: CoreDomUtilsProvider, | ||||
|             private userProvider: CoreUserProvider, private groupsProvider: CoreGroupsProvider, | ||||
|             private translate: TranslateService, private utils: CoreUtilsProvider) { | ||||
|         this.courseId = navParams.get('courseId'); | ||||
|         this.cmId = navParams.get('cmId'); | ||||
|         this.chatId = navParams.get('chatId'); | ||||
| 
 | ||||
|         this.fetchSessions().then(() => { | ||||
|             if (this.splitviewCtrl.isOn() && this.sessions.length > 0) { | ||||
|                 this.openSession(this.sessions[0]); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch chat sessions. | ||||
|      * | ||||
|      * @param {number} [showLoading] Display a loading modal. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     fetchSessions(showLoading?: boolean): Promise<any> { | ||||
|         const modal = showLoading ? this.domUtils.showModalLoading() : null; | ||||
| 
 | ||||
|         return this.groupsProvider.getActivityGroupInfo(this.cmId, false).then((groupInfo) => { | ||||
|             this.groupInfo = groupInfo; | ||||
| 
 | ||||
|             if (groupInfo.groups && groupInfo.groups.length > 0) { | ||||
|                 if (!groupInfo.groups.find((group) => group.id === this.groupId)) { | ||||
|                     this.groupId = groupInfo.groups[0].id; | ||||
|                 } | ||||
|             } else { | ||||
|                 this.groupId = 0; | ||||
|             } | ||||
| 
 | ||||
|             return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll); | ||||
|         }).then((sessions) => { | ||||
|             // Fetch user profiles.
 | ||||
|             const promises = []; | ||||
| 
 | ||||
|             sessions.forEach((session) => { | ||||
|                 session.duration = session.sessionend - session.sessionstart; | ||||
|                 session.sessionusers.forEach((sessionUser) => { | ||||
|                     if (!sessionUser.userfullname) { | ||||
|                         // The WS does not return the user name, fetch user profile.
 | ||||
|                         promises.push(this.userProvider.getProfile(sessionUser.userid, this.courseId, true).then((user) => { | ||||
|                             sessionUser.userfullname = user.fullname; | ||||
|                         }).catch(() => { | ||||
|                             // Error getting profile, most probably the user is deleted.
 | ||||
|                             sessionUser.userfullname = this.translate.instant('core.deleteduser') + ' ' + sessionUser.userid; | ||||
|                         })); | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 // If session has more than 4 users we display a "Show more" link.
 | ||||
|                 session.allsessionusers = session.sessionusers; | ||||
|                 if (session.sessionusers.length > 4) { | ||||
|                     session.sessionusers = session.allsessionusers.slice(0, 3); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             return Promise.all(promises).then(() => { | ||||
|                 this.sessions = sessions; | ||||
|             }); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|             modal && modal.dismiss(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh chat sessions. | ||||
|      * | ||||
|      * @param {any} refresher Refresher. | ||||
|      */ | ||||
|     refreshSessions(refresher: any): void { | ||||
|         const promises = [ | ||||
|             this.groupsProvider.invalidateActivityGroupInfo(this.cmId), | ||||
|             this.chatProvider.invalidateSessions(this.chatId, this.groupId, this.showAll) | ||||
|         ]; | ||||
| 
 | ||||
|         this.utils.allPromises(promises).finally(() => { | ||||
|             this.fetchSessions().finally(() => { | ||||
|                 refresher.complete(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Navigate to a session. | ||||
|      * | ||||
|      * @param {any} session Chat session. | ||||
|      */ | ||||
|     openSession(session: any): void { | ||||
|         this.selectedSessionStart = session.sessionstart; | ||||
|         this.selectedSessionGroupId = this.groupId; | ||||
|         const params = { | ||||
|             courseId: this.courseId, | ||||
|             chatId: this.chatId, | ||||
|             groupId: this.groupId, | ||||
|             sessionStart: session.sessionstart, | ||||
|             sessionEnd: session.sessionend | ||||
|         }; | ||||
|         this.splitviewCtrl.push('AddonModChatSessionMessagesPage', params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show more session users. | ||||
|      * | ||||
|      * @param {any} session Chat session. | ||||
|      * @param {Event} $event The event. | ||||
|      */ | ||||
|     showMoreUsers(session: any, $event: Event): void { | ||||
|         session.sessionusers = session.allsessionusers; | ||||
|         $event.stopPropagation(); | ||||
|     } | ||||
| } | ||||
| @ -13,8 +13,12 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreSiteWSPreSets } from '@classes/site'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features for chats. | ||||
| @ -24,33 +28,38 @@ export class AddonModChatProvider { | ||||
|     static COMPONENT = 'mmaModChat'; | ||||
|     static POLL_INTERVAL = 4000; | ||||
| 
 | ||||
|     constructor(private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider) {} | ||||
|     protected ROOT_CACHE_KEY = 'AddonModChat:'; | ||||
| 
 | ||||
|     constructor(private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, | ||||
|         private logHelper: CoreCourseLogHelperProvider, protected utils: CoreUtilsProvider, private translate: TranslateService) {} | ||||
| 
 | ||||
|     /** | ||||
|      * Get a chat. | ||||
|      * | ||||
|      * @param  {number}  courseId        Course ID. | ||||
|      * @param  {number}  cmId            Course module ID. | ||||
|      * @param  {boolean} [refresh=false] True when we should not get the value from the cache. | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @param {number} cmId Course module ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} Promise resolved when the chat is retrieved. | ||||
|      */ | ||||
|     getChat(courseId: number, cmId: number, refresh: boolean = false): Promise<any> { | ||||
|         const params = { | ||||
|             courseids: [courseId] | ||||
|         }; | ||||
|         const preSets = { | ||||
|             getFromCache: refresh ? false : undefined, | ||||
|         }; | ||||
|     getChat(courseId: number, cmId: number, siteId?: string): Promise<any> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                 courseids: [courseId] | ||||
|             }; | ||||
|             const preSets: CoreSiteWSPreSets = { | ||||
|                 cacheKey: this.getChatsCacheKey(courseId) | ||||
|             }; | ||||
| 
 | ||||
|         return this.sitesProvider.getCurrentSite().read('mod_chat_get_chats_by_courses', params, preSets).then((response) => { | ||||
|             if (response.chats) { | ||||
|                 const chat = response.chats.find((chat) => chat.coursemodule == cmId); | ||||
|                 if (chat) { | ||||
|                     return chat; | ||||
|             return site.read('mod_chat_get_chats_by_courses', params, preSets).then((response) => { | ||||
|                 if (response.chats) { | ||||
|                     const chat = response.chats.find((chat) => chat.coursemodule == cmId); | ||||
|                     if (chat) { | ||||
|                         return chat; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return Promise.reject(null); | ||||
|                 return Promise.reject(null); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -77,15 +86,16 @@ export class AddonModChatProvider { | ||||
|     /** | ||||
|      * Report a chat as being viewed. | ||||
|      * | ||||
|      * @param  {number} chatId Chat instance ID. | ||||
|      * @return {Promise<any>} Promise resolved when the WS call is executed. | ||||
|      * @param  {number} id Chat instance ID. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}  Promise resolved when the WS call is successful. | ||||
|      */ | ||||
|     logView(chatId: number): Promise<any> { | ||||
|     logView(id: number, siteId?: string): Promise<any> { | ||||
|         const params = { | ||||
|             chatid: chatId | ||||
|             chatid: id | ||||
|         }; | ||||
| 
 | ||||
|         return this.sitesProvider.getCurrentSite().write('mod_chat_view_chat', params); | ||||
|         return this.logHelper.log('mod_chat_view_chat', params, AddonModChatProvider.COMPONENT, id, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -143,8 +153,8 @@ export class AddonModChatProvider { | ||||
|                 message.userfullname = user.fullname; | ||||
|                 message.userprofileimageurl = user.profileimageurl; | ||||
|             }).catch(() => { | ||||
|                 // Error getting profile. Set default data.
 | ||||
|                 message.userfullname = message.userid; | ||||
|                 // Error getting profile, most probably the user is deleted.
 | ||||
|                 message.userfullname = this.translate.instant('core.deleteduser') + ' ' + message.userid; | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
| @ -169,4 +179,210 @@ export class AddonModChatProvider { | ||||
| 
 | ||||
|         return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return whether WS for passed sessions are available. | ||||
|      * | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>} Promise resolved with a boolean. | ||||
|      */ | ||||
|     areSessionsAvailable(siteId?: string): Promise<boolean> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return site.wsAvailable('mod_chat_get_sessions') && site.wsAvailable('mod_chat_get_session_messages'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get chat sessions. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group. | ||||
|      * @param {boolean} [showAll=false] Whether to include incomplete sessions or not. | ||||
|      * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any[]>} Promise resolved with the list of sessions. | ||||
|      * @since 3.5 | ||||
|      */ | ||||
|     getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, ignoreCache: boolean = false, siteId?: string): | ||||
|             Promise<any[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                 chatid: chatId, | ||||
|                 groupid: groupId, | ||||
|                 showall: showAll ? 1 : 0 | ||||
|             }; | ||||
|             const preSets: CoreSiteWSPreSets = { | ||||
|                 cacheKey: this.getSessionsCacheKey(chatId, groupId, showAll), | ||||
|             }; | ||||
|             if (ignoreCache) { | ||||
|                 preSets.getFromCache = false; | ||||
|                 preSets.emergencyCache = false; | ||||
|             } | ||||
| 
 | ||||
|             return site.read('mod_chat_get_sessions', params, preSets).then((response) => { | ||||
|                 if (!response || !response.sessions) { | ||||
|                     return Promise.reject(null); | ||||
|                 } | ||||
| 
 | ||||
|                 return response.sessions; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get chat session messages. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @param {number} sessionStart Session start time. | ||||
|      * @param {number} sessionEnd Session end time. | ||||
|      * @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group. | ||||
|      * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any[]>} Promise resolved with the list of messages. | ||||
|      * @since 3.5 | ||||
|      */ | ||||
|     getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, ignoreCache: boolean = false, | ||||
|             siteId?: string): Promise<any[]> { | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             const params = { | ||||
|                 chatid: chatId, | ||||
|                 sessionstart: sessionStart, | ||||
|                 sessionend: sessionEnd, | ||||
|                 groupid: groupId | ||||
|             }; | ||||
|             const preSets: CoreSiteWSPreSets = { | ||||
|                 cacheKey: this.getSessionMessagesCacheKey(chatId, sessionStart, groupId) | ||||
|             }; | ||||
|             if (ignoreCache) { | ||||
|                 preSets.getFromCache = false; | ||||
|                 preSets.emergencyCache = false; | ||||
|             } | ||||
| 
 | ||||
|             return site.read('mod_chat_get_session_messages', params, preSets).then((response) => { | ||||
|                 if (!response || !response.messages) { | ||||
|                     return Promise.reject(null); | ||||
|                 } | ||||
| 
 | ||||
|                 return response.messages; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate chats. | ||||
|      * | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateChats(courseId: number): Promise<any> { | ||||
|         const site = this.sitesProvider.getCurrentSite(); | ||||
| 
 | ||||
|         return site.invalidateWsCacheForKey(this.getChatsCacheKey(courseId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate chat sessions. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group. | ||||
|      * @param {boolean} [showAll=false] Whether to include incomplete sessions or not. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateSessions(chatId: number, groupId: number = 0, showAll: boolean = false): Promise<any> { | ||||
|         const site = this.sitesProvider.getCurrentSite(); | ||||
| 
 | ||||
|         return site.invalidateWsCacheForKey(this.getSessionsCacheKey(chatId, groupId, showAll)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate all chat sessions. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateAllSessions(chatId: number): Promise<any> { | ||||
|         const site = this.sitesProvider.getCurrentSite(); | ||||
| 
 | ||||
|         return site.invalidateWsCacheForKeyStartingWith(this.getSessionsCacheKeyPrefix(chatId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate chat session messages. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @param {number} sessionStart Session start time. | ||||
|      * @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateSessionMessages(chatId: number, sessionStart: number, groupId: number = 0): Promise<any> { | ||||
|         const site = this.sitesProvider.getCurrentSite(); | ||||
| 
 | ||||
|         return site.invalidateWsCacheForKey(this.getSessionMessagesCacheKey(chatId, sessionStart, groupId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate all chat session messages. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateAllSessionMessages(chatId: number): Promise<any> { | ||||
|         const site = this.sitesProvider.getCurrentSite(); | ||||
| 
 | ||||
|         return site.invalidateWsCacheForKeyStartingWith(this.getSessionMessagesCacheKeyPrefix(chatId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for chats WS call. | ||||
|      * | ||||
|      * @param {number} courseId Course ID. | ||||
|      * @return {string} Cache key. | ||||
|      */ | ||||
|     protected getChatsCacheKey(courseId: number): string { | ||||
|         return  this.ROOT_CACHE_KEY + 'chats:' + courseId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for sessions WS call. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @param {number} groupId Goup ID, 0 means that the function will determine the user group. | ||||
|      * @param {boolean} showAll Whether to include incomplete sessions or not. | ||||
|      * @return {string} Cache key. | ||||
|      */ | ||||
|     protected getSessionsCacheKey(chatId: number, groupId: number, showAll: boolean): string { | ||||
|         return  this.getSessionsCacheKeyPrefix(chatId) + groupId + ':' + (showAll ? 1 : 0); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key prefix for sessions WS call. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @return {string} Cache key prefix. | ||||
|      */ | ||||
|     protected getSessionsCacheKeyPrefix(chatId: number): string { | ||||
|         return  this.ROOT_CACHE_KEY + 'sessions:' + chatId + ':'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for session messages WS call. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @param {number} sessionStart Session start time. | ||||
|      * @param {number} groupId Group ID, 0 means that the function will determine the user group. | ||||
|      * @return {string} Cache key. | ||||
|      */ | ||||
|     protected getSessionMessagesCacheKey(chatId: number, sessionStart: number, groupId: number): string { | ||||
|         return this.getSessionMessagesCacheKeyPrefix(chatId) + sessionStart + ':' + groupId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key prefix for session messages WS call. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @return {string} Cache key prefix. | ||||
|      */ | ||||
|     protected getSessionMessagesCacheKeyPrefix(chatId: number): string { | ||||
|         return this.ROOT_CACHE_KEY + 'sessionsMessages:' + chatId + ':'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -18,6 +18,7 @@ import { AddonModChatIndexComponent } from '../components/index/index'; | ||||
| import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreConstants } from '@core/constants'; | ||||
| import { AddonModChatProvider } from './chat'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to support chat modules. | ||||
| @ -38,7 +39,7 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler { | ||||
|         [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true | ||||
|     }; | ||||
| 
 | ||||
|     constructor(private courseProvider: CoreCourseProvider) { } | ||||
|     constructor(private courseProvider: CoreCourseProvider, private chatProvider: AddonModChatProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled on a site level. | ||||
| @ -58,14 +59,24 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler { | ||||
|      * @return {CoreCourseModuleHandlerData} Data to render the module. | ||||
|      */ | ||||
|     getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { | ||||
|         return { | ||||
|         const data: CoreCourseModuleHandlerData = { | ||||
|             icon: this.courseProvider.getModuleIconSrc(this.modName, module.modicon), | ||||
|             title: module.name, | ||||
|             class: 'addon-mod_chat-handler', | ||||
|             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { | ||||
|                 navCtrl.push('AddonModChatIndexPage', {module: module, courseId: courseId}, options); | ||||
|             action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void { | ||||
|                 const pageParams = {module: module, courseId: courseId}; | ||||
|                 if (params) { | ||||
|                     Object.assign(pageParams, params); | ||||
|                 } | ||||
|                 navCtrl.push('AddonModChatIndexPage', pageParams, options); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         this.chatProvider.areSessionsAvailable().then((available) => { | ||||
|             data.showDownloadButton = available; | ||||
|         }); | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
							
								
								
									
										185
									
								
								src/addon/mod/chat/providers/prefetch-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/addon/mod/chat/providers/prefetch-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,185 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | ||||
| import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { AddonModChatProvider } from './chat'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to prefetch chats. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandlerBase { | ||||
|     name = 'AddonModChat'; | ||||
|     modName = 'chat'; | ||||
|     component = AddonModChatProvider.COMPONENT; | ||||
| 
 | ||||
|     constructor(translate: TranslateService, | ||||
|             appProvider: CoreAppProvider, | ||||
|             utils: CoreUtilsProvider, | ||||
|             courseProvider: CoreCourseProvider, | ||||
|             filepoolProvider: CoreFilepoolProvider, | ||||
|             sitesProvider: CoreSitesProvider, | ||||
|             domUtils: CoreDomUtilsProvider, | ||||
|             private groupsProvider: CoreGroupsProvider, | ||||
|             private userProvider: CoreUserProvider, | ||||
|             private chatProvider: AddonModChatProvider) { | ||||
| 
 | ||||
|         super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         return this.chatProvider.areSessionsAvailable(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate the prefetched content. | ||||
|      * | ||||
|      * @param {number} moduleId The module ID. | ||||
|      * @param {number} courseId The course ID the module belongs to. | ||||
|      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     invalidateContent(moduleId: number, courseId: number): Promise<any> { | ||||
|         return this.chatProvider.getChat(courseId, moduleId).then((chat) => { | ||||
|             const promises = [ | ||||
|                 this.chatProvider.invalidateAllSessions(chat.id), | ||||
|                 this.chatProvider.invalidateAllSessionMessages(chat.id) | ||||
|             ]; | ||||
| 
 | ||||
|             return this.utils.allPromises(promises); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable). | ||||
|      * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data. | ||||
|      * | ||||
|      * @param {any} module Module. | ||||
|      * @param {number} courseId Course ID the module belongs to. | ||||
|      * @return {Promise<any>} Promise resolved when invalidated. | ||||
|      */ | ||||
|     invalidateModule(module: any, courseId: number): Promise<any> { | ||||
|         const promises = [ | ||||
|             this.chatProvider.invalidateChats(courseId), | ||||
|             this.courseProvider.invalidateModule(module.id) | ||||
|         ]; | ||||
| 
 | ||||
|         return this.utils.allPromises(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch a module. | ||||
|      * | ||||
|      * @param {any} module Module. | ||||
|      * @param {number} courseId Course ID the module belongs to. | ||||
|      * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. | ||||
|      * @param {string} [dirPath] Path of the directory where to store all the content files. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> { | ||||
|         return this.prefetchPackage(module, courseId, single, this.prefetchChat.bind(this)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch a chat. | ||||
|      * | ||||
|      * @param {any} module The module object returned by WS. | ||||
|      * @param {number} courseId Course ID the module belongs to. | ||||
|      * @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section. | ||||
|      * @param {string} siteId Site ID. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { | ||||
|         // Prefetch chat and group info.
 | ||||
|         const promises = [ | ||||
|             this.chatProvider.getChat(courseId, module.id, siteId), | ||||
|             this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId) | ||||
|         ]; | ||||
| 
 | ||||
|         return Promise.all(promises).then(([chat, groupInfo]: [any, CoreGroupInfo]) => { | ||||
|             const promises = []; | ||||
| 
 | ||||
|             let groupIds = [0]; | ||||
|             if (groupInfo.groups && groupInfo.groups.length > 0) { | ||||
|                 groupIds = groupInfo.groups.map((group) => group.id); | ||||
|             } | ||||
| 
 | ||||
|             groupIds.forEach((groupId) => { | ||||
|                 // Prefetch complete sessions.
 | ||||
|                 promises.push(this.chatProvider.getSessions(chat.id, groupId, false, true, siteId).catch((error) => { | ||||
|                     // Ignore group error.
 | ||||
|                     if (error.errorcode != 'notingroup') { | ||||
|                         return Promise.reject(error); | ||||
|                     } | ||||
|                 })); | ||||
| 
 | ||||
|                 // Prefetch all sessions.
 | ||||
|                 promises.push(this.chatProvider.getSessions(chat.id, groupId, true, true, siteId).then((sessions) => { | ||||
|                     const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, siteId)); | ||||
| 
 | ||||
|                     return Promise.all(promises); | ||||
|                 }).catch((error) => { | ||||
|                     // Ignore group error.
 | ||||
|                     if (error.errorcode != 'notingroup') { | ||||
|                         return Promise.reject(error); | ||||
|                     } | ||||
|                 })); | ||||
|             }); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch chat session messages and user profiles. | ||||
|      * | ||||
|      * @param {number} chatId Chat ID. | ||||
|      * @param {any} session Session object. | ||||
|      * @param {number} groupId Group ID. | ||||
|      * @param {number} courseId Course ID the module belongs to. | ||||
|      * @param {string} siteId Site ID. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise<any> { | ||||
|         return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId) | ||||
|                 .then((messages) => { | ||||
|             const users = {}; | ||||
|             session.sessionusers.forEach((user) => { | ||||
|                 users[user.userid] = true; | ||||
|             }); | ||||
|             messages.forEach((message) => { | ||||
|                 users[message.userid] = true; | ||||
|             }); | ||||
|             const userIds = Object.keys(users).map(Number); | ||||
| 
 | ||||
|             return this.userProvider.prefetchProfiles(userIds, courseId, siteId).catch(() => { | ||||
|                 // Ignore errors, some users might not exist.
 | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
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