diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..210c47a90 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +Dockerfile diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..1003e5853 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.travis.yml b/.travis.yml index 25bb9edc0..cedbc149d 100644 --- a/.travis.yml +++ b/.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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..d884d988f --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist new file mode 100644 index 000000000..b80564cf9 --- /dev/null +++ b/GoogleService-Info.plist @@ -0,0 +1,40 @@ + + + + + AD_UNIT_ID_FOR_BANNER_TEST + ca-app-pub-3940256099942544/2934735716 + AD_UNIT_ID_FOR_INTERSTITIAL_TEST + ca-app-pub-3940256099942544/4411468910 + CLIENT_ID + 694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr + API_KEY + AIzaSyA-77ZjkxII6GV97CC9rdUl83rzdEXu_rM + GCM_SENDER_ID + 694767596569 + PLIST_VERSION + 1 + BUNDLE_ID + com.moodle.moodlemobile + PROJECT_ID + moodlemobile-push + STORAGE_BUCKET + moodlemobile-push.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:694767596569:ios:a4cdad4d168c9d1a + DATABASE_URL + https://moodlemobile-push.firebaseio.com + + \ No newline at end of file diff --git a/config.xml b/config.xml index c0e695b8f..0a1343541 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + Moodle Moodle official app Moodle Mobile team @@ -41,6 +41,7 @@ + @@ -59,8 +60,25 @@ + + + + + + + + + + + + + + + + + @@ -94,7 +112,7 @@ - + @@ -111,7 +129,7 @@ - + @@ -121,8 +139,9 @@ - - + + + diff --git a/config/copy.config.js b/config/copy.config.js index 1da283e97..7f0f0686f 100644 --- a/config/copy.config.js +++ b/config/copy.config.js @@ -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}}/' } }; diff --git a/desktop/assets/windows/AppXManifest.xml b/desktop/assets/windows/AppXManifest.xml index 61785fce3..f674c51dc 100644 --- a/desktop/assets/windows/AppXManifest.xml +++ b/desktop/assets/windows/AppXManifest.xml @@ -6,7 +6,7 @@ + Version="3.6.1.0" /> Moodle Desktop Moodle Pty Ltd. diff --git a/desktop/electron.js b/desktop/electron.js index d4a7bdbfa..343f81a5b 100644 --- a/desktop/electron.js +++ b/desktop/electron.js @@ -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(); diff --git a/google-services.json b/google-services.json new file mode 100644 index 000000000..ff50800df --- /dev/null +++ b/google-services.json @@ -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" +} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 17f687ba2..c4deb448f 100644 --- a/gulpfile.js +++ b/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(); diff --git a/hooks/after_prepare/030_android_splash.js b/hooks/after_prepare/030_android_splash.js deleted file mode 100755 index 26c1d64f3..000000000 --- a/hooks/after_prepare/030_android_splash.js +++ /dev/null @@ -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)); - } - }); -}); diff --git a/hooks/post_push b/hooks/post_push new file mode 100644 index 000000000..047dc366e --- /dev/null +++ b/hooks/post_push @@ -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 diff --git a/ionic.config.json b/ionic.config.json index a866b2bd9..52b63fde9 100644 --- a/ionic.config.json +++ b/ionic.config.json @@ -6,6 +6,5 @@ }, "type": "ionic-angular", "watchPatterns": [], - "pro_id": "com.moodle.moodlemobile", "id": "com.moodle.moodlemobile" } diff --git a/package-lock.json b/package-lock.json index 9fa7a402d..d970a674d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.6.0", + "version": "3.6.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -161,9 +161,9 @@ "integrity": "sha512-2BHO1bV4mehWZNfdsWQ/uojxYFNvk4I6u0KYnNb61RiJRY83joCEw3oFkOMRGLZthPf6TN1cueZUIAGMHXA3nA==" }, "@ionic-native/local-notifications": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@ionic-native/local-notifications/-/local-notifications-4.5.2.tgz", - "integrity": "sha512-/O2hNsWW6ixlAPY9Tw6wfIIUmNOPmd11DcxCTQ5vR8+oGPyYPj3IXkgUCI/U29Y3hDikSxdWTI19FtCxnzYKNA==" + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@ionic-native/local-notifications/-/local-notifications-4.17.0.tgz", + "integrity": "sha512-NGLGtGRduRU3f/4N2nv4hF550+NkJ9CP7mOS9vlZcZJBzlIup9X67u1M2j/+KFOpWqzS2avZ1gvZbxOmCjPNPw==" }, "@ionic-native/media-capture": { "version": "4.17.0", @@ -211,37 +211,937 @@ "integrity": "sha512-tv3R0fvOsGRHQO8ILKElG2DAJESsMsRJqdZ7VkvzepXu2WAYYMNIK/YNNJESy9sQWfGruq9aj94d6p0NMOdtng==" }, "@ionic/app-scripts": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@ionic/app-scripts/-/app-scripts-3.1.9.tgz", - "integrity": "sha512-Vf2t9X+Zu5Q+lAKNOM0cwePKul5z1qWhcpyaI/Br7/1vs/ERF+iL4gJUbfYDYC47FFPdxJmhsHrfhnv+RoXJ/A==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@ionic/app-scripts/-/app-scripts-3.2.2.tgz", + "integrity": "sha512-ZpeAafEboO/xHnmgy61ZOsaMGpoer621YX3q09x+BA2sbcuqOEsMKV91BRZmghoiHlwBB9woxXRyAwuYIAASHw==", "dev": true, "requires": { "@angular-devkit/build-optimizer": "0.0.35", - "autoprefixer": "^7.1.6", - "chalk": "^2.3.0", - "chokidar": "^1.7.0", - "clean-css": "^4.1.9", + "autoprefixer": "^7.2.6", + "chalk": "^2.4.0", + "chokidar": "^2.0.4", + "clean-css": "^4.1.11", "cross-spawn": "^5.1.0", - "express": "^4.16.2", + "dotenv-webpack": "^1.5.7", + "express": "^4.16.3", "fs-extra": "^4.0.2", "glob": "^7.1.2", "json-loader": "^0.5.7", - "node-sass": "4.7.2", + "node-sass": "^4.10.0", "os-name": "^2.0.1", - "postcss": "^6.0.13", + "postcss": "^6.0.21", "proxy-middleware": "^0.15.0", "reflect-metadata": "^0.1.10", "rollup": "0.50.0", "rollup-plugin-commonjs": "8.2.6", "rollup-plugin-node-resolve": "3.0.0", "source-map": "^0.6.1", - "tiny-lr": "^1.0.5", + "tiny-lr": "^1.1.1", "tslint": "^5.8.0", "tslint-eslint-rules": "^4.1.1", "uglify-es": "3.2.2", - "webpack": "3.8.1", + "webpack": "3.12.0", "ws": "3.3.2", "xml2js": "^0.4.19" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.1.tgz", + "integrity": "sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + } } }, "@ngx-translate/core": { @@ -312,9 +1212,9 @@ } }, "acorn": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.6.2.tgz", - "integrity": "sha512-zUzo1E5dI2Ey8+82egfnttyMlMZ2y0D8xOCO3PNPPlYXpl8NZvF6Qk9L9BEtJs+43FqEmfBViDqc5d1ckRDguw==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", "dev": true }, "acorn-dynamic-import": { @@ -347,9 +1247,9 @@ } }, "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", + "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", "dev": true }, "align-text": { @@ -668,9 +1568,9 @@ } }, "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, "assign-symbols": { @@ -680,12 +1580,12 @@ "dev": true }, "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", "dev": true, "requires": { - "lodash": "^4.17.10" + "lodash": "^4.17.11" } }, "async-done": { @@ -767,15 +1667,15 @@ } }, "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", "dev": true }, "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, "babel-code-frame": { @@ -816,6 +1716,11 @@ } } }, + "babel-plugin-add-header-comment": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/babel-plugin-add-header-comment/-/babel-plugin-add-header-comment-1.0.3.tgz", + "integrity": "sha1-URxJAQYmQNWkgLSsPt1pRBlYUOw=" + }, "bach": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", @@ -928,9 +1833,9 @@ "dev": true }, "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, "binary-extensions": { @@ -1005,30 +1910,21 @@ } }, "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", "dev": true, "requires": { "bytes": "3.0.0", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.x.x" + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" } }, "brace-expansion": { @@ -1057,9 +1953,9 @@ "dev": true }, "browser-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", "dev": true, "requires": { "resolve": "1.1.7" @@ -1099,14 +1995,15 @@ } }, "browserify-des": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", - "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", "dev": true, "requires": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", - "inherits": "^2.0.1" + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "browserify-rsa": { @@ -1320,15 +2217,15 @@ } }, "caniuse-lite": { - "version": "1.0.30000852", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000852.tgz", - "integrity": "sha512-NOuitABlrRbIpjtC8HdDnHL9Fi+yH5phDoXlXT7Im++48kll2bUps9dWWdAnBwqT/oEsjobuOLnnJCBjVqadCw==", + "version": "1.0.30000938", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000938.tgz", + "integrity": "sha512-ekW8NQ3/FvokviDxhdKLZZAx7PptXNwxKgXtnR5y+PR3hckwuP3yJ1Ir+4/c97dsHNqtAyfKUGdw8P4EYzBNgw==", "dev": true }, "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, "center-align": { @@ -1446,20 +2343,12 @@ } }, "clean-css": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", - "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", "dev": true, "requires": { - "source-map": "0.5.x" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "source-map": "~0.6.0" } }, "cli-cursor": { @@ -1598,9 +2487,9 @@ } }, "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true }, "compare-version": { @@ -2233,11 +3122,6 @@ } } }, - "cordova-plugin-app-event": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cordova-plugin-app-event/-/cordova-plugin-app-event-1.2.1.tgz", - "integrity": "sha1-DuuxQTKqQ7suXAgamr29l8otgTI=" - }, "cordova-plugin-badge": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/cordova-plugin-badge/-/cordova-plugin-badge-0.8.8.tgz", @@ -2288,10 +3172,10 @@ "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.1.3.tgz", "integrity": "sha512-6ucQ6FdlLdBm8kJfFnzozmBTjru/0xekHP/dAhjoCZggkGRlgs8TsUJFkxa/bV+qi7Dlo50JjmpE4UMWAO+aOQ==" }, - "cordova-plugin-local-notifications-mm": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/cordova-plugin-local-notifications-mm/-/cordova-plugin-local-notifications-mm-1.0.13.tgz", - "integrity": "sha512-uZjYumhkvLH6tYU7cmR9Qx9ho8xPS2/lBGgKRsejVDtBFNnkSkkR3X/at2MuBe3ZJ7qAnJdFAN4rMY3yd+dG/g==" + "cordova-plugin-local-notification": { + "version": "0.9.0-beta.3", + "resolved": "https://registry.npmjs.org/cordova-plugin-local-notification/-/cordova-plugin-local-notification-0.9.0-beta.3.tgz", + "integrity": "sha512-L3Z1velxrkm9nHFcvLnMgBPZjKFt6hwM6hn1lA+JFwIR26Yw6UF72z+/lRMBclAcOxBIDYCqeaLgvezmajjuEg==" }, "cordova-plugin-media-capture": { "version": "3.0.2", @@ -2341,6 +3225,11 @@ "resolved": "https://registry.npmjs.org/cordova-sqlite-storage-dependencies/-/cordova-sqlite-storage-dependencies-1.2.1.tgz", "integrity": "sha512-4ihQApBGVKR1QZ4oOSGctKFfthtCfiWMTcIIfxe97vKxlvGr9NyXOvYG9vXU9S7yVR7Ua+Rj47hkE7pQIKvQTg==" }, + "cordova-support-google-services": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cordova-support-google-services/-/cordova-support-google-services-1.2.1.tgz", + "integrity": "sha512-EnFjKAE9oI2uzyUvEfWpLgTM200nuJVvShaA4yyz9wMKBUN+H/BRG1byd1ibZz3sSihNKi3FxjQPxmmEn6/IfA==" + }, "core-js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", @@ -2399,15 +3288,6 @@ "which": "^1.2.9" } }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.x.x" - } - }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -2679,12 +3559,30 @@ "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==", "dev": true }, + "dotenv-defaults": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-1.0.2.tgz", + "integrity": "sha512-iXFvHtXl/hZPiFj++1hBg4lbKwGM+t/GlvELDnRtOFdjXyWP7mubkVr+eZGWG62kdsbulXAef6v/j6kiWc/xGA==", + "dev": true, + "requires": { + "dotenv": "^6.2.0" + } + }, "dotenv-expand": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.2.0.tgz", "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=", "dev": true }, + "dotenv-webpack": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-1.7.0.tgz", + "integrity": "sha512-wwNtOBW/6gLQSkb8p43y0Wts970A3xtNiG/mpwj9MLUhtPCQG6i+/DSXXoNN7fbPCU/vQ7JjwGmgOeGZSSZnsw==", + "dev": true, + "requires": { + "dotenv-defaults": "^1.0.2" + } + }, "duplexer2": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", @@ -2969,15 +3867,15 @@ } }, "electron-to-chromium": { - "version": "1.3.48", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz", - "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=", + "version": "1.3.113", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz", + "integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==", "dev": true }, "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -3233,9 +4131,9 @@ } }, "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", "dev": true }, "evp_bytestokey": { @@ -3289,14 +4187,14 @@ } }, "express": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", "dev": true, "requires": { "accepts": "~1.3.5", "array-flatten": "1.1.1", - "body-parser": "1.18.2", + "body-parser": "1.18.3", "content-disposition": "0.5.2", "content-type": "~1.0.4", "cookie": "0.3.1", @@ -3313,10 +4211,10 @@ "on-finished": "~2.3.0", "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.3", - "qs": "6.5.1", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", "range-parser": "~1.2.0", - "safe-buffer": "5.1.1", + "safe-buffer": "5.1.2", "send": "0.16.2", "serve-static": "1.13.2", "setprototypeof": "1.1.0", @@ -3331,12 +4229,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", "dev": true - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true } } }, @@ -3817,13 +4709,13 @@ "dev": true }, "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", + "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, @@ -4402,21 +5294,6 @@ "globule": "^1.0.0" } }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", @@ -5165,40 +6042,37 @@ "dev": true }, "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "dev": true, "requires": { - "chalk": "^1.1.1", - "commander": "^2.9.0", - "is-my-json-valid": "^2.12.4", - "pinkie-promise": "^2.0.0" + "ajv": "^6.5.5", + "har-schema": "^2.0.0" }, "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "ajv": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", "dev": true, "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true } } @@ -5318,25 +6192,13 @@ } }, "hash.js": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.4.tgz", - "integrity": "sha512-A6RlQvvZEtFS5fLU43IDu0QUmBy+fDO9VMdTXvufKwIkt/rFfvICAViCax5fbDO4zdNzaC3/27ZhKUok5bAJyw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dev": true, "requires": { "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.0" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" + "minimalistic-assert": "^1.0.1" } }, "hmac-drbg": { @@ -5350,12 +6212,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, "homedir-polyfill": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", @@ -5384,18 +6240,18 @@ } }, "http-parser-js": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", - "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.0.tgz", + "integrity": "sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w==", "dev": true }, "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "dev": true, "requires": { - "assert-plus": "^0.2.0", + "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } @@ -5407,10 +6263,13 @@ "dev": true }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } }, "ieee754": { "version": "1.1.12", @@ -5465,6 +6324,11 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, + "install": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/install/-/install-0.8.9.tgz", + "integrity": "sha1-n0tcDRhR74cunfheT3Fi1OXc2+0=" + }, "interpret": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", @@ -5478,9 +6342,9 @@ "dev": true }, "ionic-angular": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/ionic-angular/-/ionic-angular-3.9.2.tgz", - "integrity": "sha512-BEZ6magY1i5GwM9ki/MOpszUz62+g518HsGICtw9TE1D4v9Eb6n/o7e+X0vtvpK4TdouFjQ8r5XA9VPAKW9/+Q==" + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/ionic-angular/-/ionic-angular-3.9.3.tgz", + "integrity": "sha512-EOuz9nyu0lV4KbqN+HipXuhoMRMDfprZeckg7H1Z+9AZpK9d7VzaiweyRNG5iaLp+cGCzBIQCfWhwlCo2vyuUA==" }, "ionicons": { "version": "3.0.0", @@ -5488,9 +6352,9 @@ "integrity": "sha1-QLja9P16MRUL0AIWD2ZJbiKpjDw=" }, "ipaddr.js": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", - "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", "dev": true }, "is-absolute": { @@ -5642,25 +6506,6 @@ "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", "dev": true }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, "is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", @@ -5719,12 +6564,6 @@ "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -5821,9 +6660,9 @@ "dev": true }, "js-base64": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", - "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", "dev": true }, "js-tokens": { @@ -5883,10 +6722,13 @@ "dev": true }, "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } }, "jsonfile": { "version": "4.0.0", @@ -5903,12 +6745,6 @@ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "dev": true }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -6054,9 +6890,9 @@ } }, "livereload-js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.3.0.tgz", - "integrity": "sha512-j1R0/FeGa64Y+NmqfZhyoVRzcFlOZ8sNlKzHjh4VvLULFACZhn68XrX5DFg2FhMvSMJmROuFxRSa560ECWKBMg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", + "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", "dev": true }, "load-json-file": { @@ -6073,20 +6909,20 @@ } }, "loader-runner": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", - "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", "dev": true }, "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", "dev": true, "requires": { - "big.js": "^3.1.3", + "big.js": "^5.2.2", "emojis-list": "^2.0.0", - "json5": "^0.5.0" + "json5": "^1.0.1" } }, "locate-path": { @@ -6108,9 +6944,9 @@ } }, "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, "lodash._basecopy": { @@ -6282,9 +7118,9 @@ } }, "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, "requires": { "pseudomap": "^1.0.2", @@ -6633,18 +7469,19 @@ } }, "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==" }, "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, "requires": { "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "media-typer": { @@ -6739,18 +7576,18 @@ "dev": true }, "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==", "dev": true }, "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", "dev": true, "requires": { - "mime-db": "~1.33.0" + "mime-db": "~1.38.0" } }, "mimic-fn": { @@ -6898,9 +7735,9 @@ "dev": true }, "neo-async": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.1.tgz", - "integrity": "sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", + "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", "dev": true }, "next-tick": { @@ -7094,9 +7931,9 @@ } }, "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", + "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==", "dev": true, "requires": { "assert": "^1.1.1", @@ -7106,7 +7943,7 @@ "constants-browserify": "^1.0.0", "crypto-browserify": "^3.11.0", "domain-browser": "^1.1.1", - "events": "^1.0.0", + "events": "^3.0.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "0.0.0", @@ -7120,7 +7957,7 @@ "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", "url": "^0.11.0", - "util": "^0.10.3", + "util": "^0.11.0", "vm-browserify": "0.0.4" } }, @@ -7131,9 +7968,9 @@ "dev": true }, "node-sass": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", - "integrity": "sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz", + "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -7148,10 +7985,10 @@ "lodash.mergewith": "^4.6.0", "meow": "^3.7.0", "mkdirp": "^0.5.1", - "nan": "^2.3.2", - "node-gyp": "^3.3.1", + "nan": "^2.10.0", + "node-gyp": "^3.8.0", "npmlog": "^4.0.0", - "request": "~2.79.0", + "request": "^2.88.0", "sass-graph": "^2.2.4", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" @@ -7272,9 +8109,9 @@ "dev": true }, "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, "object-assign": { @@ -7567,16 +8404,17 @@ "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" }, "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", "dev": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parse-filepath": { @@ -7708,9 +8546,9 @@ } }, "pbkdf2": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", - "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -7731,9 +8569,18 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "phonegap-plugin-multidex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/phonegap-plugin-multidex/-/phonegap-plugin-multidex-1.0.0.tgz", + "integrity": "sha512-1wvc3iQOQpEBaQbXgLxA2JUiLSQ2azdF/bF29ghXDiQJWSpQ1BF8gSuqttM8WZoj081Ps8OKL0gYxdDBkFNPqA==" + }, "phonegap-plugin-push": { - "version": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#cf2ed2075d9d2d58a4c4f79543f669ed6366c148", - "from": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle" + "version": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#9b1d9fe575d1f21b517327c480e7fe0f73280e7a", + "from": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v2", + "requires": { + "babel-plugin-add-header-comment": "^1.0.3", + "install": "^0.8.2" + } }, "pify": { "version": "2.3.0", @@ -7826,9 +8673,9 @@ "dev": true }, "postcss": { - "version": "6.0.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz", - "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==", + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", "dev": true, "requires": { "chalk": "^2.4.1", @@ -7837,9 +8684,9 @@ } }, "postcss-value-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", "dev": true }, "preserve": { @@ -7875,13 +8722,13 @@ } }, "proxy-addr": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", - "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", "dev": true, "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.6.0" + "ipaddr.js": "1.8.0" } }, "proxy-middleware": { @@ -7909,16 +8756,17 @@ "dev": true }, "public-encrypt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", - "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "dev": true, "requires": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1" + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "pump": { @@ -7949,9 +8797,9 @@ "dev": true }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, "querystring": { @@ -7967,9 +8815,9 @@ "dev": true }, "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", "requires": { "is-number": "^4.0.0", "kind-of": "^6.0.0", @@ -8014,41 +8862,15 @@ "dev": true }, "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", "dev": true, "requires": { "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", - "dev": true - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "dev": true, - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": ">= 1.3.1 < 2" - } - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", - "dev": true - } } }, "read-config-file": { @@ -8260,37 +9082,37 @@ } }, "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "dev": true, "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.11.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~2.0.6", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "qs": "~6.3.0", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "~0.4.1", - "uuid": "^3.0.0" + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" }, "dependencies": { - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true } } @@ -8417,13 +9239,295 @@ } }, "rollup-pluginutils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.3.0.tgz", - "integrity": "sha512-xB6hsRsjdJdIYWEyYUJy/3ki5g69wrf0luHPGNK3ZSocV6HLNfio59l3dZ3TL4xUwEKgROhFi9jOCt6c5gfUWw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.4.1.tgz", + "integrity": "sha512-wesMQ9/172IJDIW/lYWm0vW0LiKe5Ekjws481R7z9WTRtmO59cqyM/2uUlxvf6yzm/fElFmHUobeQOYz46dZJw==", "dev": true, "requires": { - "estree-walker": "^0.5.2", - "micromatch": "^2.3.11" + "estree-walker": "^0.6.0", + "micromatch": "^3.1.10" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "estree-walker": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.0.tgz", + "integrity": "sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw==", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } } }, "rxjs": { @@ -8757,19 +9861,10 @@ "kind-of": "^3.2.0" } }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, "source-list-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", "dev": true }, "source-map": { @@ -8934,18 +10029,18 @@ "dev": true }, "stdout-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", - "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", "dev": true, "requires": { "readable-stream": "^2.0.1" } }, "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dev": true, "requires": { "inherits": "~2.0.1", @@ -9002,12 +10097,6 @@ "safe-buffer": "~5.1.0" } }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", - "dev": true - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -9084,9 +10173,9 @@ "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" }, "tapable": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", - "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz", + "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==", "dev": true }, "tar": { @@ -9168,13 +10257,19 @@ }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true } } }, @@ -9246,11 +10341,12 @@ } }, "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "dev": true, "requires": { + "psl": "^1.1.24", "punycode": "^1.4.1" } }, @@ -9261,27 +10357,12 @@ "dev": true }, "true-case-path": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", - "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", "dev": true, "requires": { - "glob": "^6.0.4" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "glob": "^7.1.2" } }, "truncate-utf8-bytes": { @@ -9315,9 +10396,9 @@ "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==" }, "tslint": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz", - "integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.1.tgz", + "integrity": "sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==", "dev": true, "requires": { "babel-code-frame": "^6.22.0", @@ -9331,7 +10412,7 @@ "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.8.0", - "tsutils": "^2.12.1" + "tsutils": "^2.27.2" } }, "tslint-eslint-rules": { @@ -9354,9 +10435,9 @@ } }, "tsutils": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", - "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -9369,10 +10450,13 @@ "dev": true }, "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } }, "tweetnacl": { "version": "0.14.5", @@ -9696,9 +10780,9 @@ "dev": true }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { "inherits": "2.0.3" @@ -9716,9 +10800,9 @@ "dev": true }, "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true }, "v8flags": { @@ -9859,6 +10943,17 @@ "requires": { "micromatch": "^3.1.4", "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } } }, "arr-diff": { @@ -9903,23 +10998,23 @@ } }, "chokidar": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", - "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.1.tgz", + "integrity": "sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ==", "dev": true, "requires": { "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.1.2", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", - "inherits": "^2.0.1", + "inherits": "^2.0.3", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", - "normalize-path": "^2.1.1", + "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.0" + "readdirp": "^2.2.1", + "upath": "^1.1.0" } }, "expand-brackets": { @@ -10073,6 +11168,535 @@ } } }, + "fsevents": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -10190,6 +11814,23 @@ "snapdragon": "^0.8.1", "to-regex": "^3.0.2" } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } } } }, @@ -10199,15 +11840,15 @@ "integrity": "sha1-Om2bwVGWN3qQ+OKAP6UmIWWwRRA=" }, "webpack": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz", - "integrity": "sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.12.0.tgz", + "integrity": "sha512-Sw7MdIIOv/nkzPzee4o0EdvCuPmxT98+vVpIvwtcwcF1Q4SDSNp92vwcKc4REe7NItH9f1S4ra9FuQ7yuYZ8bQ==", "dev": true, "requires": { "acorn": "^5.0.0", "acorn-dynamic-import": "^2.0.0", - "ajv": "^5.1.5", - "ajv-keywords": "^2.0.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", "async": "^2.1.2", "enhanced-resolve": "^3.4.0", "escope": "^3.6.0", @@ -10228,6 +11869,18 @@ "yargs": "^8.0.2" }, "dependencies": { + "ajv": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -10240,6 +11893,12 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -10261,6 +11920,18 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -10402,9 +12073,9 @@ } }, "webpack-sources": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", - "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", "dev": true, "requires": { "source-list-map": "^2.0.0", diff --git a/package.json b/package.json index c5361461b..221a31a47 100644 --- a/package.json +++ b/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", diff --git a/resources/android/icon/drawable-hdpi-smallicon.png b/resources/android/icon/drawable-hdpi-smallicon.png new file mode 100644 index 000000000..5262dcb0f Binary files /dev/null and b/resources/android/icon/drawable-hdpi-smallicon.png differ diff --git a/resources/android/icon/drawable-ldpi-smallicon.png b/resources/android/icon/drawable-ldpi-smallicon.png new file mode 100644 index 000000000..3360a685b Binary files /dev/null and b/resources/android/icon/drawable-ldpi-smallicon.png differ diff --git a/resources/android/icon/drawable-mdpi-smallicon.png b/resources/android/icon/drawable-mdpi-smallicon.png new file mode 100644 index 000000000..c8df8f94d Binary files /dev/null and b/resources/android/icon/drawable-mdpi-smallicon.png differ diff --git a/resources/android/icon/drawable-xhdpi-smallicon.png b/resources/android/icon/drawable-xhdpi-smallicon.png new file mode 100644 index 000000000..28081d204 Binary files /dev/null and b/resources/android/icon/drawable-xhdpi-smallicon.png differ diff --git a/scripts/aot.sh b/scripts/aot.sh index 5246a8010..f10a70790 100755 --- a/scripts/aot.sh +++ b/scripts/aot.sh @@ -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 + diff --git a/scripts/langindex.json b/scripts/langindex.json index 22b33e825..d86efa26d 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -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", diff --git a/scripts/linux.sh b/scripts/linux.sh new file mode 100755 index 000000000..ef76cd0a5 --- /dev/null +++ b/scripts/linux.sh @@ -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 diff --git a/src/addon/badges/providers/mybadges-link-handler.ts b/src/addon/badges/providers/mybadges-link-handler.ts index 8c58e384d..8a36cbbc0 100644 --- a/src/addon/badges/providers/mybadges-link-handler.ts +++ b/src/addon/badges/providers/mybadges-link-handler.ts @@ -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 { diff --git a/src/addon/blog/blog.module.ts b/src/addon/blog/blog.module.ts new file mode 100644 index 000000000..f31372745 --- /dev/null +++ b/src/addon/blog/blog.module.ts @@ -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); + } +} diff --git a/src/addon/blog/components/components.module.ts b/src/addon/blog/components/components.module.ts new file mode 100644 index 000000000..0e56fcc3f --- /dev/null +++ b/src/addon/blog/components/components.module.ts @@ -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 {} diff --git a/src/addon/blog/components/entries/addon-blog-entries.html b/src/addon/blog/components/entries/addon-blog-entries.html new file mode 100644 index 000000000..9312a00cd --- /dev/null +++ b/src/addon/blog/components/entries/addon-blog-entries.html @@ -0,0 +1,49 @@ + + + + + +
+ + {{ 'addon.blog.showonlyyourentries' | translate }} + + +
+ + + + + +

+ + + {{ 'addon.blog.' + entry.publishTranslated | translate}} + +

+

+ + {{entry.created | coreDateDayOrTime}} + + {{entry.user && entry.user.fullname}} +

+
+ + + + + + + {{ 'addon.blog.linktooriginalentry' | translate }} + + + + + {{entry.lastmodified | coreTimeAgo}} + + + +
+
+ +
+
diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts new file mode 100644 index 000000000..54f84d30a --- /dev/null +++ b/src/addon/blog/components/entries/entries.ts @@ -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} Promise with the entries. + */ + private fetchEntries(refresh: boolean = false): Promise { + 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} Resolved when done. + */ + loadMore(infiniteComplete?: any): Promise { + 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(); + } + }); + }); + } + +} diff --git a/src/addon/blog/lang/en.json b/src/addon/blog/lang/en.json new file mode 100644 index 000000000..6e183232f --- /dev/null +++ b/src/addon/blog/lang/en.json @@ -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" +} \ No newline at end of file diff --git a/src/addon/blog/pages/entries/entries.html b/src/addon/blog/pages/entries/entries.html new file mode 100644 index 000000000..e5a12aba8 --- /dev/null +++ b/src/addon/blog/pages/entries/entries.html @@ -0,0 +1,7 @@ + + + {{ title | translate }} + + + + diff --git a/src/addon/blog/pages/entries/entries.module.ts b/src/addon/blog/pages/entries/entries.module.ts new file mode 100644 index 000000000..a9cb5564f --- /dev/null +++ b/src/addon/blog/pages/entries/entries.module.ts @@ -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 {} diff --git a/src/addon/blog/pages/entries/entries.ts b/src/addon/blog/pages/entries/entries.ts new file mode 100644 index 000000000..220de15df --- /dev/null +++ b/src/addon/blog/pages/entries/entries.ts @@ -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'; + } + } +} diff --git a/src/addon/blog/providers/blog.ts b/src/addon/blog/providers/blog.ts new file mode 100644 index 000000000..adc15b1f2 --- /dev/null +++ b/src/addon/blog/providers/blog.ts @@ -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} Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + isPluginEnabled(siteId?: string): Promise { + 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} Promise to be resolved when the entries are retrieved. + */ + getEntries(filter: any = {}, page: number = 0, siteId?: string): Promise { + 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} Promise resolved when data is invalidated. + */ + invalidateEntries(filter: any = {}, siteId?: string): Promise { + 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} Promise to be resolved when done. + */ + logView(filter: any = {}, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value') + }; + + return site.write('core_blog_view_entries', data); + }); + } +} diff --git a/src/addon/blog/providers/course-option-handler.ts b/src/addon/blog/providers/course-option-handler.ts new file mode 100644 index 000000000..9c28973fb --- /dev/null +++ b/src/addon/blog/providers/course-option-handler.ts @@ -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} Promise resolved when done. + */ + invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise { + 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 { + 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} True or promise resolved with true if enabled. + */ + isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { + 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} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { + 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} Promise resolved when done. + */ + prefetch(course: any): Promise { + 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(); + }); + }); + } +} diff --git a/src/addon/blog/providers/index-link-handler.ts b/src/addon/blog/providers/index-link-handler.ts new file mode 100644 index 000000000..176aec76e --- /dev/null +++ b/src/addon/blog/providers/index-link-handler.ts @@ -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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + 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} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + + return this.blogProvider.isPluginEnabled(siteId); + } +} diff --git a/src/addon/blog/providers/mainmenu-handler.ts b/src/addon/blog/providers/mainmenu-handler.ts new file mode 100644 index 000000000..e45bfd06f --- /dev/null +++ b/src/addon/blog/providers/mainmenu-handler.ts @@ -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 { + 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' + }; + } +} diff --git a/src/addon/blog/providers/user-handler.ts b/src/addon/blog/providers/user-handler.ts new file mode 100644 index 000000000..039b9ed56 --- /dev/null +++ b/src/addon/blog/providers/user-handler.ts @@ -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} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + 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} Promise resolved with true if enabled, resolved with false otherwise. + */ + isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise { + 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 }); + } + }; + } +} diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index 85aa71eb2..3146e6e69 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -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. diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 3342aaeeb..6ccb04caa 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -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", diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index a9711f678..c00376085 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -10,10 +10,10 @@ - + - - +

+

{{ 'addon.calendar.eventstarttime' | translate}}

{{ event.timestart * 1000 | coreFormatDate }}

@@ -52,21 +52,29 @@
- + - {{ 'addon.calendar.notifications' | translate }} - - {{ 'core.defaultvalue' | translate :{$a: defaultTimeReadable} }} - {{ 'core.settings.disabled' | translate }} - {{ 600 | coreDuration }} - {{ 1800 | coreDuration }} - {{ 3600 | coreDuration }} - {{ 7200 | coreDuration }} - {{ 21600 | coreDuration }} - {{ 43200 | coreDuration }} - {{ 86400 | coreDuration }} - +

{{ 'addon.calendar.reminders' | translate }}

+ + +

{{ 'core.defaultvalue' | translate :{$a: ((event.timestart - defaultTime) * 1000) | coreFormatDate } }}

+

{{ reminder.time * 1000 | coreFormatDate }}

+ +
+
+ + + + {{ 'addon.calendar.setnewreminder' | translate }} + + + + + +
diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index e20a61877..568eac629 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -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. * diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index b02765ed0..14ad8d69b 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -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 | 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} Promise resolved when done. + * @return {Promise} Promise resolved when done. */ - cleanExpiredEvents(siteId?: string): Promise { + cleanExpiredEvents(siteId?: string): Promise { 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} Resolved when done. + */ + protected deleteEvent(eventId: number, siteId?: string): Promise { + 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} Event notification time in minutes. 0 if disabled. + * @return {Promise} Promise resolved when the notification is updated. */ - getEventNotificationTime(id: number, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + addEventReminder(event: any, time: number, siteId?: string): Promise { + 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} Promise resolved when the notification is updated. + */ + deleteEventReminder(id: number, siteId?: string): Promise { + 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} Promise with wvent notification time in minutes. 0 if disabled, -1 if default time. + * @return {Promise} Promise resolved when the event data is retrieved. */ - getEventNotificationTimeOption(id: number, siteId?: string): Promise { - return this.getEventFromLocalDb(id, siteId).then((e) => { - return e.notificationtime || -1; - }).catch(() => { - return -1; + getEventReminders(id: number, siteId?: string): Promise { + 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} Promise resolved when the notification is scheduled. */ - scheduleEventNotification(event: any, time: number, siteId?: string): Promise { + protected scheduleEventNotification(event: any, reminderId: number, time: number, siteId?: string): Promise { 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} Promise resolved when all the notifications have been scheduled. */ scheduleEventsNotifications(events: any[], siteId?: string): Promise { - 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 { 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} Promise resolved when the notification is updated. - */ - updateNotificationTime(event: any, time: number, siteId?: string): Promise { - 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); + })); }); } } diff --git a/src/addon/coursecompletion/components/report/addon-course-completion-report.html b/src/addon/coursecompletion/components/report/addon-course-completion-report.html index b4a6a25eb..2b4b92070 100644 --- a/src/addon/coursecompletion/components/report/addon-course-completion-report.html +++ b/src/addon/coursecompletion/components/report/addon-course-completion-report.html @@ -3,7 +3,7 @@ - +

{{ 'addon.coursecompletion.status' | translate }}

{{ completion.statusText | translate }}

@@ -14,7 +14,7 @@

{{ 'addon.coursecompletion.criteriarequiredany' | translate }}

- + {{ 'addon.coursecompletion.requiredcriteria' | translate }}

@@ -41,11 +41,16 @@
- + {{ 'addon.coursecompletion.manualselfcompletion' | translate }} + +
+ + {{ 'addon.coursecompletion.nottracked' | translate }} +
diff --git a/src/addon/coursecompletion/components/report/report.ts b/src/addon/coursecompletion/components/report/report.ts index 69583770f..50d12cf90 100644 --- a/src/addon/coursecompletion/components/report/report.ts +++ b/src/addon/coursecompletion/components/report/report.ts @@ -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); + } }); } diff --git a/src/addon/coursecompletion/lang/en.json b/src/addon/coursecompletion/lang/en.json index 7607702c6..81ef0272e 100644 --- a/src/addon/coursecompletion/lang/en.json +++ b/src/addon/coursecompletion/lang/en.json @@ -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", diff --git a/src/addon/messages/components/discussions/discussions.ts b/src/addon/messages/components/discussions/discussions.ts index bf3eb9f21..b072b719b 100644 --- a/src/addon/messages/components/discussions/discussions.ts +++ b/src/addon/messages/components/discussions/discussions.ts @@ -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 { 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) => { diff --git a/src/addon/messages/lang/en.json b/src/addon/messages/lang/en.json index 2ec162f66..1b542a0c7 100644 --- a/src/addon/messages/lang/en.json +++ b/src/addon/messages/lang/en.json @@ -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}}" } \ No newline at end of file diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts index 43b3609c6..9ea151d83 100644 --- a/src/addon/messages/messages.module.ts +++ b/src/addon/messages/messages.module.ts @@ -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); }); }); }); diff --git a/src/addon/messages/pages/conversation-info/conversation-info.html b/src/addon/messages/pages/conversation-info/conversation-info.html index 36af28369..393b078b6 100644 --- a/src/addon/messages/pages/conversation-info/conversation-info.html +++ b/src/addon/messages/pages/conversation-info/conversation-info.html @@ -27,7 +27,7 @@

- +

diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index b3151026e..25cdcab13 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -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. diff --git a/src/addon/messages/pages/group-conversations/group-conversations.html b/src/addon/messages/pages/group-conversations/group-conversations.html index e4d916c38..10d5c3052 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.html +++ b/src/addon/messages/pages/group-conversations/group-conversations.html @@ -100,7 +100,7 @@

- +

{{ conversation.unreadcount }} diff --git a/src/addon/messages/pages/group-conversations/group-conversations.ts b/src/addon/messages/pages/group-conversations/group-conversations.ts index 39be42305..332bae82c 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.ts +++ b/src/addon/messages/pages/group-conversations/group-conversations.ts @@ -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 { // 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 { // 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(() => { diff --git a/src/addon/messages/pages/search/search.html b/src/addon/messages/pages/search/search.html index 43dfbd4c5..4a9ba7621 100644 --- a/src/addon/messages/pages/search/search.html +++ b/src/addon/messages/pages/search/search.html @@ -34,7 +34,7 @@

- +

{{result.lastmessagedate | coreDateDayOrTime}} diff --git a/src/addon/messages/pages/settings/settings.html b/src/addon/messages/pages/settings/settings.html index 4048bb8f6..0172888d9 100644 --- a/src/addon/messages/pages/settings/settings.html +++ b/src/addon/messages/pages/settings/settings.html @@ -32,6 +32,7 @@ +
@@ -90,5 +91,20 @@
+ + + + + {{ 'core.settings.general' | translate }} + + +

{{ 'addon.messages.useentertosend' | translate }}

+

{{ 'addon.messages.useentertosenddescdesktop' | translate }}

+

{{ 'addon.messages.useentertosenddescmac' | translate }}

+
+ +
+
+
diff --git a/src/addon/messages/pages/settings/settings.ts b/src/addon/messages/pages/settings/settings.ts index 7b524ff3c..d7e30cd01 100644 --- a/src/addon/messages/pages/settings/settings.ts +++ b/src/addon/messages/pages/settings/settings.ts @@ -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. */ diff --git a/src/addon/messages/providers/messages-offline.ts b/src/addon/messages/providers/messages-offline.ts index f340c7e17..38737c50e 100644 --- a/src/addon/messages/providers/messages-offline.ts +++ b/src/addon/messages/providers/messages-offline.ts @@ -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); } /** diff --git a/src/addon/mod/assign/components/index/addon-mod-assign-index.html b/src/addon/mod/assign/components/index/addon-mod-assign-index.html index fd9fd8488..edf43166f 100644 --- a/src/addon/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addon/mod/assign/components/index/addon-mod-assign-index.html @@ -3,6 +3,7 @@ + @@ -31,7 +32,15 @@ - + + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + +

{{ 'addon.mod_assign.timeremaining' | translate }}

{{ timeRemaining }}

@@ -79,7 +88,7 @@ {{ 'addon.mod_assign.ungroupedusers' | translate }} -
+ diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts index 23d2ebda0..7cd42a015 100644 --- a/src/addon/mod/assign/components/index/index.ts +++ b/src/addon/mod/assign/components/index/index.ts @@ -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} Resolved when done. + */ + setGroup(groupId: number): Promise { + 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)); } } diff --git a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html index ea03450af..84e54a560 100644 --- a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html @@ -215,6 +215,12 @@

{{ feedback.gradeddate * 1000 | coreFormatDate }}

+ + +

{{ 'addon.mod_assign.gradedon' | translate }}

+

{{ feedback.gradeddate * 1000 | coreFormatDate }}

+
+
diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index cffefe88d..c3f655855 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -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. diff --git a/src/addon/mod/assign/pages/edit/edit.ts b/src/addon/mod/assign/pages/edit/edit.ts index e98d6abf0..f6072f48f 100644 --- a/src/addon/mod/assign/pages/edit/edit.ts +++ b/src/addon/mod/assign/pages/edit/edit.ts @@ -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, diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.html b/src/addon/mod/assign/pages/submission-list/submission-list.html index 8a0ff95ef..cd96434da 100644 --- a/src/addon/mod/assign/pages/submission-list/submission-list.html +++ b/src/addon/mod/assign/pages/submission-list/submission-list.html @@ -15,10 +15,17 @@ + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + - - + +

{{submission.userfullname}}

{{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}

diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.ts b/src/addon/mod/assign/pages/submission-list/submission-list.ts index d5103ccf3..8ce653613 100644 --- a/src/addon/mod/assign/pages/submission-list/submission-list.ts +++ b/src/addon/mod/assign/pages/submission-list/submission-list.ts @@ -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} Promise resolved when done. */ protected fetchAssignment(): Promise { - 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} Resolved when done. + */ + setGroup(groupId: number): Promise { + 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, diff --git a/src/addon/mod/assign/pages/submission-review/submission-review.ts b/src/addon/mod/assign/pages/submission-review/submission-review.ts index be53916dc..44b0ca4a9 100644 --- a/src/addon/mod/assign/pages/submission-review/submission-review.ts +++ b/src/addon/mod/assign/pages/submission-review/submission-review.ts @@ -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(() => { diff --git a/src/addon/mod/assign/providers/assign-offline.ts b/src/addon/mod/assign/providers/assign-offline.ts index 6c54503b7..87d630837 100644 --- a/src/addon/mod/assign/providers/assign-offline.ts +++ b/src/addon/mod/assign/providers/assign-offline.ts @@ -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); } /** diff --git a/src/addon/mod/assign/providers/assign-sync.ts b/src/addon/mod/assign/providers/assign-sync.ts index 96627a1cc..cff8c421a 100644 --- a/src/addon/mod/assign/providers/assign-sync.ts +++ b/src/addon/mod/assign/providers/assign-sync.ts @@ -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. diff --git a/src/addon/mod/assign/providers/assign.ts b/src/addon/mod/assign/providers/assign.ts index c6c3bf8cf..93e8204f0 100644 --- a/src/addon/mod/assign/providers/assign.ts +++ b/src/addon/mod/assign/providers/assign.ts @@ -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} Promise resolved with the assignment. */ - getAssignment(courseId: number, cmId: number, siteId?: string): Promise { - return this.getAssignmentByField(courseId, 'cmid', cmId, siteId); + getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { + 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} Promise resolved when the assignment is retrieved. */ - protected getAssignmentByField(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getAssignmentByField(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) + : Promise { + 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} Promise resolved with the assignment. */ - getAssignmentById(courseId: number, id: number, siteId?: string): Promise { - return this.getAssignmentByField(courseId, 'id', id, siteId); + getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { + 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} Promise resolved with the user blind id. */ - getAssignmentUserMappings(assignId: number, userId: number, siteId?: string): Promise { + getAssignmentUserMappings(assignId: number, userId: number, ignoreCache?: boolean, siteId?: string): Promise { 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} Resolved with requested info when done. */ - getAssignmentGrades(assignId: number, siteId?: string): Promise { + getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise { 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} Promise always resolved with the user submission status. */ - getSubmissionStatus(assignId: number, userId?: number, isBlind?: boolean, filter: boolean = true, ignoreCache?: boolean, - siteId?: string): Promise { + getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, + ignoreCache?: boolean, siteId?: string): Promise { 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} 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 { + + 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} Promise always resolved. Resolve param is the formatted submissions. */ getSubmissionsUserData(submissions: any[], courseId: number, assignId: number, blind?: boolean, participants?: any[], - siteId?: string): Promise { + ignoreCache?: boolean, siteId?: string): Promise { 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} Promise resolved with the list of participants and summary of submissions. */ - listParticipants(assignId: number, groupId?: number, siteId?: string): Promise { + listParticipants(assignId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise { 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 { 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} Promise resolved when the data is invalidated. */ - invalidateSubmissionStatusData(assignId: number, userId?: number, isBlind?: boolean, siteId?: string): Promise { + invalidateSubmissionStatusData(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, siteId?: string): + Promise { 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} Promise resolved when the WS call is successful. */ logGradingView(assignId: number, siteId?: string): Promise { - 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} Promise resolved when the WS call is successful. */ logView(assignId: number, siteId?: string): Promise { - 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; diff --git a/src/addon/mod/assign/providers/helper.ts b/src/addon/mod/assign/providers/helper.ts index d227634de..6b64d5c1b 100644 --- a/src/addon/mod/assign/providers/helper.ts +++ b/src/addon/mod/assign/providers/helper.ts @@ -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 { + getParticipants(assign: any, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise { + 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; diff --git a/src/addon/mod/assign/providers/module-handler.ts b/src/addon/mod/assign/providers/module-handler.ts index 937aeeb1d..1aa0cda0d 100644 --- a/src/addon/mod/assign/providers/module-handler.ts +++ b/src/addon/mod/assign/providers/module-handler.ts @@ -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); } }; } diff --git a/src/addon/mod/assign/providers/prefetch-handler.ts b/src/addon/mod/assign/providers/prefetch-handler.ts index d7a86836a..cae9b6fec 100644 --- a/src/addon/mod/assign/providers/prefetch-handler.ts +++ b/src/addon/mod/assign/providers/prefetch-handler.ts @@ -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 { - 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} Promise resolved when invalidated. + */ + invalidateModule(module: any, courseId: number): Promise { + 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} Promise resolved when prefetched, rejected otherwise. */ protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise { - // 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. + })); }); } } diff --git a/src/addon/mod/assign/submission/onlinetext/lang/en.json b/src/addon/mod/assign/submission/onlinetext/lang/en.json index 9b8a3d9f9..e49362133 100644 --- a/src/addon/mod/assign/submission/onlinetext/lang/en.json +++ b/src/addon/mod/assign/submission/onlinetext/lang/en.json @@ -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." } \ No newline at end of file diff --git a/src/addon/mod/assign/submission/onlinetext/providers/handler.ts b/src/addon/mod/assign/submission/onlinetext/providers/handler.ts index 718a13f51..80745907b 100644 --- a/src/addon/mod/assign/submission/onlinetext/providers/handler.ts +++ b/src/addon/mod/assign/submission/onlinetext/providers/handler.ts @@ -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); diff --git a/src/addon/mod/book/components/components.module.ts b/src/addon/mod/book/components/components.module.ts index b0ace2f26..54e83ef50 100644 --- a/src/addon/mod/book/components/components.module.ts +++ b/src/addon/mod/book/components/components.module.ts @@ -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 {} diff --git a/src/addon/mod/book/components/index/addon-mod-book-index.html b/src/addon/mod/book/components/index/addon-mod-book-index.html index 32c49d009..b5ce9efc2 100644 --- a/src/addon/mod/book/components/index/addon-mod-book-index.html +++ b/src/addon/mod/book/components/index/addon-mod-book-index.html @@ -1,11 +1,12 @@ - + diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 573a53778..75ad3beb1 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -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); } diff --git a/src/addon/mod/book/components/toc-popover/addon-mod-assign-submission-toc-popover.html b/src/addon/mod/book/components/toc-popover/addon-mod-assign-submission-toc-popover.html deleted file mode 100644 index 6d3dca6f0..000000000 --- a/src/addon/mod/book/components/toc-popover/addon-mod-assign-submission-toc-popover.html +++ /dev/null @@ -1,5 +0,0 @@ - - -

{{chapter.title}}

- -
diff --git a/src/addon/mod/book/lang/en.json b/src/addon/mod/book/lang/en.json index bf58bf921..7d1140fe4 100644 --- a/src/addon/mod/book/lang/en.json +++ b/src/addon/mod/book/lang/en.json @@ -1,4 +1,5 @@ { "errorchapter": "Error reading chapter of book.", - "modulenameplural": "Books" + "modulenameplural": "Books", + "toc": "Table of contents" } \ No newline at end of file diff --git a/src/addon/mod/book/pages/index/index.html b/src/addon/mod/book/pages/index/index.html index 14f0a66bd..78c828426 100644 --- a/src/addon/mod/book/pages/index/index.html +++ b/src/addon/mod/book/pages/index/index.html @@ -12,5 +12,5 @@ - + diff --git a/src/addon/mod/book/pages/index/index.ts b/src/addon/mod/book/pages/index/index.ts index a45a34e1d..7bb9e1f0f 100644 --- a/src/addon/mod/book/pages/index/index.ts +++ b/src/addon/mod/book/pages/index/index.ts @@ -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; } diff --git a/src/addon/mod/book/pages/toc/toc.html b/src/addon/mod/book/pages/toc/toc.html new file mode 100644 index 000000000..30e22bea0 --- /dev/null +++ b/src/addon/mod/book/pages/toc/toc.html @@ -0,0 +1,19 @@ + + + {{ 'addon.mod_book.toc' | translate }} + + + + + + + + diff --git a/src/addon/mod/book/pages/toc/toc.module.ts b/src/addon/mod/book/pages/toc/toc.module.ts new file mode 100644 index 000000000..88e436a25 --- /dev/null +++ b/src/addon/mod/book/pages/toc/toc.module.ts @@ -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 {} diff --git a/src/addon/mod/book/components/toc-popover/toc-popover.ts b/src/addon/mod/book/pages/toc/toc.ts similarity index 71% rename from src/addon/mod/book/components/toc-popover/toc-popover.ts rename to src/addon/mod/book/pages/toc/toc.ts index 416044a3f..8048eacb8 100644 --- a/src/addon/mod/book/components/toc-popover/toc-popover.ts +++ b/src/addon/mod/book/pages/toc/toc.ts @@ -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(); + } } diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts index 4f97a6e83..e322284c6 100644 --- a/src/addon/mod/book/providers/book.ts +++ b/src/addon/mod/book/providers/book.ts @@ -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} Promise resolved when the WS call is successful. */ - logView(id: number, chapterId: string): Promise { + logView(id: number, chapterId: string, siteId?: string): Promise { 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); } } diff --git a/src/addon/mod/book/providers/link-handler.ts b/src/addon/mod/book/providers/link-handler.ts index 1035ef3f9..899978d4b 100644 --- a/src/addon/mod/book/providers/link-handler.ts +++ b/src/addon/mod/book/providers/link-handler.ts @@ -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} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + + 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); + } + }]; + } } diff --git a/src/addon/mod/book/providers/module-handler.ts b/src/addon/mod/book/providers/module-handler.ts index a1b4c9af4..6f92d6f32 100644 --- a/src/addon/mod/book/providers/module-handler.ts +++ b/src/addon/mod/book/providers/module-handler.ts @@ -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); } }; } diff --git a/src/addon/mod/chat/chat.module.ts b/src/addon/mod/chat/chat.module.ts index 62ce5d80b..7d90566e0 100644 --- a/src/addon/mod/chat/chat.module.ts +++ b/src/addon/mod/chat/chat.module.ts @@ -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); } } diff --git a/src/addon/mod/chat/components/index/addon-mod-chat-index.html b/src/addon/mod/chat/components/index/addon-mod-chat-index.html index 43432d53b..f76b8f8f8 100644 --- a/src/addon/mod/chat/components/index/addon-mod-chat-index.html +++ b/src/addon/mod/chat/components/index/addon-mod-chat-index.html @@ -3,7 +3,9 @@ + + @@ -16,7 +18,8 @@ {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }} -
+ diff --git a/src/addon/mod/chat/components/index/index.ts b/src/addon/mod/chat/components/index/index.ts index 34f3bc248..a04cf11d0 100644 --- a/src/addon/mod/chat/components/index/index.ts +++ b/src/addon/mod/chat/components/index/index.ts @@ -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}); + } } diff --git a/src/addon/mod/chat/lang/en.json b/src/addon/mod/chat/lang/en.json index 30f9613ca..4348b5d63 100644 --- a/src/addon/mod/chat/lang/en.json +++ b/src/addon/mod/chat/lang/en.json @@ -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" } \ No newline at end of file diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.html b/src/addon/mod/chat/pages/session-messages/session-messages.html new file mode 100644 index 000000000..a315ebecf --- /dev/null +++ b/src/addon/mod/chat/pages/session-messages/session-messages.html @@ -0,0 +1,40 @@ + + + {{ 'addon.mod_chat.messages' | translate }} + + + + + + + +
+
+ + {{ message.timestamp * 1000 | coreFormatDate:"strftimedayshort" }} + +
+ +
+ + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }} + +
+ +
+ + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }} + +
+ + + +

+

{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }}

+ +

+ +
+
+
+
diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.module.ts b/src/addon/mod/chat/pages/session-messages/session-messages.module.ts new file mode 100644 index 000000000..816b70999 --- /dev/null +++ b/src/addon/mod/chat/pages/session-messages/session-messages.module.ts @@ -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 {} diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.scss b/src/addon/mod/chat/pages/session-messages/session-messages.scss new file mode 100644 index 000000000..a8d1e96e8 --- /dev/null +++ b/src/addon/mod/chat/pages/session-messages/session-messages.scss @@ -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; + } +} diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.ts b/src/addon/mod/chat/pages/session-messages/session-messages.ts new file mode 100644 index 000000000..91fe8874b --- /dev/null +++ b/src/addon/mod/chat/pages/session-messages/session-messages.ts @@ -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} Promise resolved when done. + */ + protected fetchMessages(): Promise { + 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'); + } +} diff --git a/src/addon/mod/chat/pages/sessions/sessions.html b/src/addon/mod/chat/pages/sessions/sessions.html new file mode 100644 index 000000000..b78df54bc --- /dev/null +++ b/src/addon/mod/chat/pages/sessions/sessions.html @@ -0,0 +1,45 @@ + + + {{ 'addon.mod_chat.chatreport' | translate }} + + + + + + + + + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + + {{ 'addon.mod_chat.showincompletesessions' | translate }} + + + + +

{{ session.sessionstart * 1000 | coreFormatDate }}

+

{{ session.duration | coreDuration }}

+
+ +

+ {{ user.userfullname }} ({{ user.messagecount }}) +

+
+
+ +
+
+ + +
+
+
diff --git a/src/addon/mod/chat/pages/sessions/sessions.module.ts b/src/addon/mod/chat/pages/sessions/sessions.module.ts new file mode 100644 index 000000000..23e5cdc1e --- /dev/null +++ b/src/addon/mod/chat/pages/sessions/sessions.module.ts @@ -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 {} diff --git a/src/addon/mod/chat/pages/sessions/sessions.scss b/src/addon/mod/chat/pages/sessions/sessions.scss new file mode 100644 index 000000000..066605cdc --- /dev/null +++ b/src/addon/mod/chat/pages/sessions/sessions.scss @@ -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; + } +} diff --git a/src/addon/mod/chat/pages/sessions/sessions.ts b/src/addon/mod/chat/pages/sessions/sessions.ts new file mode 100644 index 000000000..35f26cb33 --- /dev/null +++ b/src/addon/mod/chat/pages/sessions/sessions.ts @@ -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} Promise resolved when done. + */ + fetchSessions(showLoading?: boolean): Promise { + 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(); + } +} diff --git a/src/addon/mod/chat/providers/chat.ts b/src/addon/mod/chat/providers/chat.ts index d4f6e8f3e..f12107dc4 100644 --- a/src/addon/mod/chat/providers/chat.ts +++ b/src/addon/mod/chat/providers/chat.ts @@ -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} Promise resolved when the chat is retrieved. */ - getChat(courseId: number, cmId: number, refresh: boolean = false): Promise { - const params = { - courseids: [courseId] - }; - const preSets = { - getFromCache: refresh ? false : undefined, - }; + getChat(courseId: number, cmId: number, siteId?: string): Promise { + 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} 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} Promise resolved when the WS call is successful. */ - logView(chatId: number): Promise { + logView(id: number, siteId?: string): Promise { 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} Promise resolved with a boolean. + */ + areSessionsAvailable(siteId?: string): Promise { + 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} 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 { + 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} 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 { + 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} Promise resolved when the data is invalidated. + */ + invalidateChats(courseId: number): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateSessions(chatId: number, groupId: number = 0, showAll: boolean = false): Promise { + const site = this.sitesProvider.getCurrentSite(); + + return site.invalidateWsCacheForKey(this.getSessionsCacheKey(chatId, groupId, showAll)); + } + + /** + * Invalidate all chat sessions. + * + * @param {number} chatId Chat ID. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllSessions(chatId: number): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateSessionMessages(chatId: number, sessionStart: number, groupId: number = 0): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateAllSessionMessages(chatId: number): Promise { + 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 + ':'; + } } diff --git a/src/addon/mod/chat/providers/module-handler.ts b/src/addon/mod/chat/providers/module-handler.ts index e54d0fda2..f85c2be5e 100644 --- a/src/addon/mod/chat/providers/module-handler.ts +++ b/src/addon/mod/chat/providers/module-handler.ts @@ -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; } /** diff --git a/src/addon/mod/chat/providers/prefetch-handler.ts b/src/addon/mod/chat/providers/prefetch-handler.ts new file mode 100644 index 000000000..c9f54a62a --- /dev/null +++ b/src/addon/mod/chat/providers/prefetch-handler.ts @@ -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} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + 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} Promise resolved when invalidated. + */ + invalidateModule(module: any, courseId: number): Promise { + 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} Promise resolved when done. + */ + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + 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} Promise resolved when done. + */ + protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise { + // 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} Promise resolved when done. + */ + protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise { + return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId) + .then((messages) => { + 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. + }); + }); + } +} diff --git a/src/addon/mod/choice/components/index/addon-mod-choice-index.html b/src/addon/mod/choice/components/index/addon-mod-choice-index.html index 58eeaefe7..59a85ab7b 100644 --- a/src/addon/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addon/mod/choice/components/index/addon-mod-choice-index.html @@ -3,6 +3,7 @@ + diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index fe583dd04..fb2243153 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -352,14 +352,13 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo } const modal = this.domUtils.showModalLoading('core.sending', true); - this.choiceProvider.submitResponse(this.choice.id, this.choice.name, this.courseId, responses).then(() => { + this.choiceProvider.submitResponse(this.choice.id, this.choice.name, this.courseId, responses).then((online) => { // Success! // Check completion since it could be configured to complete once the user answers the choice. this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); this.domUtils.scrollToTop(this.content); - // Let's refresh the data. - return this.refreshContent(false); + return this.dataUpdated(online); }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true); }).finally(() => { @@ -377,7 +376,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.choiceProvider.deleteResponses(this.choice.id, this.choice.name, this.courseId).then(() => { this.domUtils.scrollToTop(this.content); - // Success! Let's refresh the data. + // Refresh the data. Don't call dataUpdated because deleting an answer doesn't mark the choice as outdated. return this.refreshContent(false); }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true); @@ -389,6 +388,28 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo }); } + /** + * Function to call when some data has changed. It will refresh/prefetch data. + * + * @param {boolean} online Whether the data was sent to server or stored in offline. + * @return {Promise} Promise resolved when done. + */ + protected dataUpdated(online: boolean): Promise { + if (online && this.isPrefetched()) { + // The choice is downloaded, update the data. + return this.choiceSync.prefetchAfterUpdate(this.module, this.courseId).then(() => { + // Update the view. + this.showLoadingAndFetch(false, false); + }).catch(() => { + // Prefetch failed, refresh the data. + return this.refreshContent(false); + }); + } else { + // Not downloaded, refresh the data. + return this.refreshContent(false); + } + } + /** * Performs the sync of the activity. * diff --git a/src/addon/mod/choice/lang/en.json b/src/addon/mod/choice/lang/en.json index a0714b34a..e5c48508f 100644 --- a/src/addon/mod/choice/lang/en.json +++ b/src/addon/mod/choice/lang/en.json @@ -2,11 +2,11 @@ "cannotsubmit": "Sorry, there was a problem submitting your choice. Please try again.", "choiceoptions": "Choice options", "errorgetchoice": "Error getting choice data.", - "expired": "Sorry, this activity closed on {{$a}} and is no longer available", + "expired": "This activity closed on {{$a}}.", "full": "(Full)", "modulenameplural": "Choices", "noresultsviewable": "The results are not currently viewable.", - "notopenyet": "Sorry, this activity is not available until {{$a}}", + "notopenyet": "This activity is not available until {{$a}}.", "numberofuser": "Number of responses", "numberofuserinpercentage": "Percentage of responses", "previewonly": "This is just a preview of the available options for this activity. You will not be able to submit your choice until {{$a}}.", diff --git a/src/addon/mod/choice/providers/choice.ts b/src/addon/mod/choice/providers/choice.ts index b94fec312..b94e92b9c 100644 --- a/src/addon/mod/choice/providers/choice.ts +++ b/src/addon/mod/choice/providers/choice.ts @@ -17,7 +17,9 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModChoiceOfflineProvider } from './offline'; +import { CoreSiteWSPreSets } from '@classes/site'; /** * Service that provides some features for choices. @@ -38,7 +40,7 @@ export class AddonModChoiceProvider { constructor(private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private filepoolProvider: CoreFilepoolProvider, private utils: CoreUtilsProvider, - private choiceOffline: AddonModChoiceOfflineProvider) {} + private choiceOffline: AddonModChoiceOfflineProvider, private logHelper: CoreCourseLogHelperProvider) {} /** * Check if results can be seen by a student. The student can see the results if: @@ -67,9 +69,9 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID the choice belongs to. * @param {number[]} [responses] IDs of the answers. If not defined, delete all the answers of the current user. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when the options are deleted. + * @return {Promise} Promise resolved with boolean: true if response was sent to server, false if stored in device. */ - deleteResponses(choiceId: number, name: string, courseId: number, responses?: number[], siteId?: string): Promise { + deleteResponses(choiceId: number, name: string, courseId: number, responses?: number[], siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); responses = responses || []; @@ -172,20 +174,29 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {string} key Name of the property to check. * @param {any} value Value to search. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache: boolean = false) - : Promise { + protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache?: boolean, + ignoreCache?: boolean): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceDataCacheKey(courseId), omitExpires: forceCache }; + if (forceCache) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choices_by_courses', params, preSets).then((response) => { if (response && response.choices) { const currentChoice = response.choices.find((choice) => choice[key] == value); @@ -205,11 +216,12 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {number} cmId Course module ID. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - getChoice(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache); + getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache); } /** @@ -218,29 +230,36 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {number} choiceId Choice ID. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache); + getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache); } /** * Get choice options. * - * @param {number} choiceId Choice ID. - * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} choiceId Choice 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} Promise resolved with choice options. */ - getOptions(choiceId: number, siteId?: string): Promise { + getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceOptionsCacheKey(choiceId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choice_options', params, preSets).then((response) => { if (response.options) { return response.options; @@ -254,19 +273,25 @@ export class AddonModChoiceProvider { /** * Get choice results. * - * @param {number} choiceId Choice ID. - * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} choiceId Choice 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} Promise resolved with choice results. */ - getResults(choiceId: number, siteId?: string): Promise { + getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceResultsCacheKey(choiceId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choice_results', params, preSets).then((response) => { if (response.options) { return response.options; @@ -346,14 +371,15 @@ export class AddonModChoiceProvider { * Report the choice as being viewed. * * @param {string} id Choice ID. - * @return {Promise} Promise resolved when the WS call is successful. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the WS call is successful. */ - logView(id: string): Promise { + logView(id: number, siteId?: string): Promise { const params = { choiceid: id }; - return this.sitesProvider.getCurrentSite().write('mod_choice_view_choice', params); + return this.logHelper.log('mod_choice_view_choice', params, AddonModChoiceProvider.COMPONENT, id, siteId); } /** diff --git a/src/addon/mod/choice/providers/module-handler.ts b/src/addon/mod/choice/providers/module-handler.ts index 163b595a5..cc2448da9 100644 --- a/src/addon/mod/choice/providers/module-handler.ts +++ b/src/addon/mod/choice/providers/module-handler.ts @@ -64,8 +64,12 @@ export class AddonModChoiceModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_choice-handler', showDownloadButton: true, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModChoiceIndexPage', {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('AddonModChoiceIndexPage', pageParams, options); } }; } diff --git a/src/addon/mod/choice/providers/offline.ts b/src/addon/mod/choice/providers/offline.ts index f17e1d2ed..b4193f380 100644 --- a/src/addon/mod/choice/providers/offline.ts +++ b/src/addon/mod/choice/providers/offline.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; /** * Service to handle offline choices. @@ -23,45 +23,50 @@ export class AddonModChoiceOfflineProvider { // Variables for database. static CHOICE_TABLE = 'addon_mod_choice_responses'; - protected tablesSchema = [ - { - name: AddonModChoiceOfflineProvider.CHOICE_TABLE, - columns: [ - { - name: 'choiceid', - type: 'INTEGER' - }, - { - name: 'name', - type: 'TEXT' - }, - { - name: 'courseid', - type: 'INTEGER' - }, - { - name: 'userid', - type: 'INTEGER' - }, - { - name: 'responses', - type: 'TEXT' - }, - { - name: 'deleting', - type: 'INTEGER' - }, - { - name: 'timecreated', - type: 'INTEGER' - } - ], - primaryKeys: ['choiceid', 'userid'] - } - ]; + + protected siteSchema: CoreSiteSchema = { + name: 'AddonModChoiceOfflineProvider', + version: 1, + tables: [ + { + name: AddonModChoiceOfflineProvider.CHOICE_TABLE, + columns: [ + { + name: 'choiceid', + type: 'INTEGER' + }, + { + name: 'name', + type: 'TEXT' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'userid', + type: 'INTEGER' + }, + { + name: 'responses', + type: 'TEXT' + }, + { + name: 'deleting', + type: 'INTEGER' + }, + { + name: 'timecreated', + type: 'INTEGER' + } + ], + primaryKeys: ['choiceid', 'userid'] + } + ] + }; constructor(private sitesProvider: CoreSitesProvider) { - this.sitesProvider.createTablesFromSchema(this.tablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); } /** diff --git a/src/addon/mod/choice/providers/prefetch-handler.ts b/src/addon/mod/choice/providers/prefetch-handler.ts index fb3680e4c..0b2f74eba 100644 --- a/src/addon/mod/choice/providers/prefetch-handler.ts +++ b/src/addon/mod/choice/providers/prefetch-handler.ts @@ -23,7 +23,6 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonModChoiceProvider } from './choice'; -import { AddonModChoiceSyncProvider } from './sync'; /** * Handler to prefetch choices. @@ -38,7 +37,7 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan constructor(translate: TranslateService, appProvider: CoreAppProvider, utils: CoreUtilsProvider, courseProvider: CoreCourseProvider, filepoolProvider: CoreFilepoolProvider, sitesProvider: CoreSitesProvider, domUtils: CoreDomUtilsProvider, protected choiceProvider: AddonModChoiceProvider, - protected syncProvider: AddonModChoiceSyncProvider, protected userProvider: CoreUserProvider) { + protected userProvider: CoreUserProvider) { super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); } @@ -66,12 +65,12 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan * @return {Promise} Promise resolved when done. */ protected prefetchChoice(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.choiceProvider.getChoice(courseId, module.id, siteId).then((choice) => { + return this.choiceProvider.getChoice(courseId, module.id, siteId, false, true).then((choice) => { const promises = []; // Get the options and results. - promises.push(this.choiceProvider.getOptions(choice.id, siteId)); - promises.push(this.choiceProvider.getResults(choice.id, siteId).then((options) => { + promises.push(this.choiceProvider.getOptions(choice.id, true, siteId)); + promises.push(this.choiceProvider.getResults(choice.id, true, siteId).then((options) => { // If we can see the users that answered, prefetch their profile and avatar. const subPromises = []; options.forEach((option) => { diff --git a/src/addon/mod/choice/providers/sync.ts b/src/addon/mod/choice/providers/sync.ts index 01635c5d4..9a14b71d4 100644 --- a/src/addon/mod/choice/providers/sync.ts +++ b/src/addon/mod/choice/providers/sync.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreSitesProvider } from '@providers/sites'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -25,13 +24,17 @@ import { AddonModChoiceProvider } from './choice'; import { CoreEventsProvider } from '@providers/events'; import { TranslateService } from '@ngx-translate/core'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { CoreSyncProvider } from '@providers/sync'; +import { AddonModChoicePrefetchHandler } from './prefetch-handler'; /** * Service to sync choices. */ @Injectable() -export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { +export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_choice_autom_synced'; protected componentTranslate: string; @@ -40,9 +43,12 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { protected appProvider: CoreAppProvider, private choiceOffline: AddonModChoiceOfflineProvider, private eventsProvider: CoreEventsProvider, private choiceProvider: AddonModChoiceProvider, translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, - courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider, timeUtils: CoreTimeUtilsProvider) { + private courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider, timeUtils: CoreTimeUtilsProvider, + private logHelper: CoreCourseLogHelperProvider, prefetchHandler: AddonModChoicePrefetchHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate) { + super('AddonModChoiceSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('choice'); } @@ -137,10 +143,14 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { updated: false }; - // Get offline responses to be sent. - const syncPromise = this.choiceOffline.getResponse(choiceId, siteId, userId).catch(() => { - // No offline data found, return empty object. - return {}; + // Sync offline logs. + const syncPromise = this.logHelper.syncIfNeeded(AddonModChoiceProvider.COMPONENT, choiceId, siteId).catch(() => { + // Ignore errors. + }).then(() => { + return this.choiceOffline.getResponse(choiceId, siteId, userId).catch(() => { + // No offline data found, return empty object. + return {}; + }); }).then((data) => { if (!data.choiceid) { // Nothing to sync. @@ -189,15 +199,9 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { }); }).then(() => { if (courseId) { - const promises = [ - this.choiceProvider.invalidateChoiceData(courseId), - choiceId ? this.choiceProvider.invalidateOptions(choiceId) : Promise.resolve(), - choiceId ? this.choiceProvider.invalidateResults(choiceId) : Promise.resolve(), - ]; - - // Data has been sent to server, update choice data. - return Promise.all(promises).then(() => { - return this.choiceProvider.getChoiceById(courseId, choiceId, siteId); + // Data has been sent to server, prefetch choice if needed. + return this.courseProvider.getModuleBasicInfoByInstance(choiceId, 'choice', siteId).then((module) => { + return this.prefetchAfterUpdate(module, courseId, undefined, siteId); }).catch(() => { // Ignore errors. }); diff --git a/src/addon/mod/data/components/action/action.ts b/src/addon/mod/data/components/action/action.ts index dab8a35d0..32be60ec3 100644 --- a/src/addon/mod/data/components/action/action.ts +++ b/src/addon/mod/data/components/action/action.ts @@ -30,6 +30,7 @@ export class AddonModDataActionComponent implements OnInit { @Input() action: string; // The field to render. @Input() entry?: any; // The value of the field. @Input() database: any; // Database object. + @Input() offset?: number; // Offset of the entry. siteId: string; rootUrl: string; @@ -67,6 +68,9 @@ export class AddonModDataActionComponent implements OnInit { switch (this.action) { case 'more': this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id; + if (typeof this.offset == 'number') { + this.url += '&mode=single&page=' + this.offset; + } break; case 'edit': this.url = this.rootUrl + '/mod/data/edit.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id; diff --git a/src/addon/mod/data/components/index/addon-mod-data-index.html b/src/addon/mod/data/components/index/addon-mod-data-index.html index 94b658e30..d6c306669 100644 --- a/src/addon/mod/data/components/index/addon-mod-data-index.html +++ b/src/addon/mod/data/components/index/addon-mod-data-index.html @@ -6,8 +6,9 @@ - - + + + @@ -21,7 +22,7 @@ -
+
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
@@ -66,9 +67,7 @@
- +
diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index 9e32c7afb..ddd5975a9 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -19,6 +19,9 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; +import { CoreRatingProvider } from '@core/rating/providers/rating'; +import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; +import { CoreRatingSyncProvider } from '@core/rating/providers/sync'; import { AddonModDataProvider } from '../../providers/data'; import { AddonModDataHelperProvider } from '../../providers/helper'; import { AddonModDataOfflineProvider } from '../../providers/offline'; @@ -47,7 +50,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp timeAvailableToReadable: string | boolean; isEmpty = false; groupInfo: CoreGroupInfo; - entries = {}; + entries = []; firstEntry = false; canAdd = false; canSearch = false; @@ -64,7 +67,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp offlineActions: any; offlineEntries: any; entriesRendered = ''; - cssTemplate = ''; extraImports = [AddonModDataComponentsModule]; jsData; foundRecordsData; @@ -74,11 +76,16 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp protected hasComments = false; protected fieldsArray: any; + hasOfflineRatings: boolean; + protected ratingOfflineObserver: any; + protected ratingSyncObserver: any; + constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider, private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content, private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider, private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, - private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController) { + private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController, + private ratingOffline: CoreRatingOfflineProvider) { super(injector, content); // Refresh entries on change. @@ -89,7 +96,22 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp return this.loadContent(true); } }, this.siteId); + + // Listen for offline ratings saved and synced. + this.ratingOfflineObserver = this.eventsProvider.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => { + if (this.data && data.component == 'mod_data' && data.ratingArea == 'entry' && data.contextLevel == 'module' + && data.instanceId == this.data.coursemodule) { + this.hasOfflineRatings = true; + } + }); + this.ratingSyncObserver = this.eventsProvider.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => { + if (this.data && data.component == 'mod_data' && data.ratingArea == 'entry' && data.contextLevel == 'module' + && data.instanceId == this.data.coursemodule) { + this.hasOfflineRatings = false; + } + }); } + /** * Component being initialized. */ @@ -275,8 +297,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp } if (!this.isEmpty) { - this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.addon-data-entries-' + this.data.id); - const siteInfo = this.sitesProvider.getCurrentSite().getInfo(), promises = []; @@ -317,6 +337,8 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp }); return Promise.all(promises).then((entries) => { + this.entries = entries; + let entriesHTML = this.data.listtemplateheader || ''; // Get first entry from the whole list. @@ -326,12 +348,15 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp const template = this.data.listtemplate || this.dataHelper.getDefaultTemplate('list', this.fieldsArray); - entries.forEach((entry) => { - this.entries[entry.id] = entry; + const entriesById = {}; + entries.forEach((entry, index) => { + entriesById[entry.id] = entry; const actions = this.dataHelper.getActions(this.data, this.access, entry); + const offset = this.search.page * AddonModDataProvider.PER_PAGE + index; - entriesHTML += this.dataHelper.displayShowFields(template, this.fieldsArray, entry, 'list', actions); + entriesHTML += this.dataHelper.displayShowFields(template, this.fieldsArray, entry, offset, 'list', + actions); }); entriesHTML += this.data.listtemplatefooter || ''; @@ -340,7 +365,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp // Pass the input data to the component. this.jsData = { fields: this.fields, - entries: this.entries, + entries: entriesById, data: this.data, gotoEntry: this.gotoEntry.bind(this) }; @@ -440,9 +465,16 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp module: this.module, courseId: this.courseId, entryId: entryId, - group: this.selectedGroup + group: this.selectedGroup, + offset: null }; + // Try to find page number and offset of the entry. + const pageXOffset = this.entries.findIndex((entry) => entry.id == entryId); + if (pageXOffset >= 0) { + params.offset = this.search.page * AddonModDataProvider.PER_PAGE + pageXOffset; + } + this.navCtrl.push('AddonModDataEntryPage', params); } @@ -475,6 +507,10 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp } }); } + }).then(() => { + return this.ratingOffline.hasRatings('mod_data', 'entry', 'module', this.data.coursemodule).then((hasRatings) => { + this.hasOfflineRatings = hasRatings; + }); }); } @@ -484,7 +520,17 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * @return {Promise} Promise resolved when done. */ protected sync(): Promise { - return this.dataSync.syncDatabase(this.data.id); + const promises = [ + this.dataSync.syncDatabase(this.data.id), + this.dataSync.syncRatings(this.data.coursemodule) + ]; + + return Promise.all(promises).then((results) => { + return results.reduce((a, b) => ({ + updated: a.updated || b.updated, + warnings: (a.warnings || []).concat(b.warnings || []), + }), {updated: false}); + }); } /** @@ -503,5 +549,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp ngOnDestroy(): void { super.ngOnDestroy(); this.entryChangedObserver && this.entryChangedObserver.off(); + this.ratingOfflineObserver && this.ratingOfflineObserver.off(); + this.ratingSyncObserver && this.ratingSyncObserver.off(); } } diff --git a/src/addon/mod/data/fields/picture/component/addon-mod-data-field-picture.html b/src/addon/mod/data/fields/picture/component/addon-mod-data-field-picture.html index d0bbb6885..c5a4a560e 100644 --- a/src/addon/mod/data/fields/picture/component/addon-mod-data-field-picture.html +++ b/src/addon/mod/data/fields/picture/component/addon-mod-data-field-picture.html @@ -11,6 +11,6 @@ - + - + diff --git a/src/addon/mod/data/fields/url/providers/handler.ts b/src/addon/mod/data/fields/url/providers/handler.ts index ae8eb55d5..1854df829 100644 --- a/src/addon/mod/data/fields/url/providers/handler.ts +++ b/src/addon/mod/data/fields/url/providers/handler.ts @@ -54,7 +54,7 @@ export class AddonModDataFieldUrlHandler extends AddonModDataFieldTextHandler { { fieldid: field.id, subfield: '0', - value: inputData[fieldName] || '' + value: (inputData[fieldName] && inputData[fieldName].trim()) || '' } ]; } diff --git a/src/addon/mod/data/pages/edit/edit.html b/src/addon/mod/data/pages/edit/edit.html index 19ca035f7..2e7994fc3 100644 --- a/src/addon/mod/data/pages/edit/edit.html +++ b/src/addon/mod/data/pages/edit/edit.html @@ -18,10 +18,8 @@ -
- +
+
diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index 5520c8e2c..4ca834934 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -57,7 +57,6 @@ export class AddonModDataEditPage { loaded = false; selectedGroup = 0; cssClass = ''; - cssTemplate = ''; groupInfo: any; editFormRender = ''; editForm: FormGroup; @@ -132,8 +131,6 @@ export class AddonModDataEditPage { return this.dataProvider.getDatabaseAccessInformation(data.id); }).then((accessData) => { - this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass); - if (this.entryId) { return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule, accessData.canmanageentries) .then((groupInfo) => { diff --git a/src/addon/mod/data/pages/entry/entry.html b/src/addon/mod/data/pages/entry/entry.html index c1d727795..1767dc9c0 100644 --- a/src/addon/mod/data/pages/entry/entry.html +++ b/src/addon/mod/data/pages/entry/entry.html @@ -22,28 +22,29 @@ -
- +
+
+ + + - + - - - - diff --git a/src/addon/mod/data/pages/entry/entry.module.ts b/src/addon/mod/data/pages/entry/entry.module.ts index cebf202e2..eb77745c6 100644 --- a/src/addon/mod/data/pages/entry/entry.module.ts +++ b/src/addon/mod/data/pages/entry/entry.module.ts @@ -19,6 +19,7 @@ import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreComponentsModule } from '@components/components.module'; import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; +import { CoreRatingComponentsModule } from '@core/rating/components/components.module'; import { AddonModDataComponentsModule } from '../../components/components.module'; import { AddonModDataEntryPage } from './entry'; @@ -33,7 +34,8 @@ import { AddonModDataEntryPage } from './entry'; CoreCompileHtmlComponentModule, CoreCommentsComponentsModule, IonicPageModule.forChild(AddonModDataEntryPage), - TranslateModule.forChild() + TranslateModule.forChild(), + CoreRatingComponentsModule ], }) export class AddonModDataEntryPageModule {} diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 8ea967f72..6f6cf7649 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -20,6 +20,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreEventsProvider } from '@providers/events'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreRatingInfo } from '@core/rating/providers/rating'; import { AddonModDataProvider } from '../../providers/data'; import { AddonModDataHelperProvider } from '../../providers/helper'; import { AddonModDataOfflineProvider } from '../../providers/offline'; @@ -41,7 +42,7 @@ export class AddonModDataEntryPage implements OnDestroy { protected module: any; protected entryId: number; protected courseId: number; - protected page: number; + protected offset: number; protected syncObserver: any; // It will observe the sync auto event. protected entryChangedObserver: any; // It will observe the changed entry event. protected fields = {}; @@ -54,18 +55,17 @@ export class AddonModDataEntryPage implements OnDestroy { entry: any; offlineActions = []; hasOffline = false; - cssTemplate = ''; - previousId: number; - nextId: number; + previousOffset: number; + nextOffset: number; access: any; data: any; groupInfo: any; showComments: any; entryRendered = ''; siteId: string; - cssClass = ''; extraImports = [AddonModDataComponentsModule]; jsData; + ratingInfo: CoreRatingInfo; constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, @@ -77,7 +77,7 @@ export class AddonModDataEntryPage implements OnDestroy { this.entryId = params.get('entryId') || null; this.courseId = params.get('courseId'); this.selectedGroup = params.get('group') || 0; - this.page = params.get('page') || null; + this.offset = params.get('offset'); this.siteId = sitesProvider.getCurrentSiteId(); @@ -131,9 +131,8 @@ export class AddonModDataEntryPage implements OnDestroy { return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { this.title = data.name || this.title; this.data = data; - this.cssClass = 'addon-data-entries-' + data.id; - return this.setEntryIdFromPage(data.id, this.page, this.selectedGroup).then(() => { + return this.setEntryIdFromOffset(data.id, this.offset, this.selectedGroup).then(() => { return this.dataProvider.getDatabaseAccessInformation(data.id); }); }).then((accessData) => { @@ -162,8 +161,8 @@ export class AddonModDataEntryPage implements OnDestroy { return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions); }); }).then((entry) => { + this.ratingInfo = entry.ratinginfo; entry = entry.entry; - this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass); // Index contents by fieldid. entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); @@ -177,7 +176,7 @@ export class AddonModDataEntryPage implements OnDestroy { const actions = this.dataHelper.getActions(this.data, this.access, this.entry); const templte = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', fieldsArray); - this.entryRendered = this.dataHelper.displayShowFields(templte, fieldsArray, this.entry, 'show', actions); + this.entryRendered = this.dataHelper.displayShowFields(templte, fieldsArray, this.entry, this.offset, 'show', actions); this.showComments = actions.comments; const entries = {}; @@ -189,11 +188,6 @@ export class AddonModDataEntryPage implements OnDestroy { entries: entries, data: this.data }; - - return this.dataHelper.getPageInfoByEntry(this.data.id, this.entryId, this.selectedGroup).then((result) => { - this.previousId = result.previousId; - this.nextId = result.nextId; - }); }).catch((message) => { if (!refresh) { // Some call failed, retry without using cache since it might be a new activity. @@ -210,13 +204,13 @@ export class AddonModDataEntryPage implements OnDestroy { /** * Go to selected entry without changing state. * - * @param {number} entry Entry Id where to go. - * @return {Promise} Resolved when done. + * @param {number} offset Entry offset. + * @return {Promise} Resolved when done. */ - gotoEntry(entry: number): Promise { - this.entryId = entry; + gotoEntry(offset: number): Promise { + this.offset = offset; + this.entryId = null; this.entry = null; - this.page = null; this.entryLoaded = false; return this.fetchEntryData(); @@ -264,30 +258,63 @@ export class AddonModDataEntryPage implements OnDestroy { */ setGroup(groupId: number): Promise { this.selectedGroup = groupId; + this.offset = 0; + this.entry = null; + this.entryId = null; this.entryLoaded = false; - return this.setEntryIdFromPage(this.data.id, 0, this.selectedGroup).then(() => { - return this.fetchEntryData(); + return this.fetchEntryData(); + } + + /** + * Convenience function to translate offset to entry identifier and set next/previous entries. + * + * @param {number} dataId Data Id. + * @param {number} [offset] Offset of the entry. + * @param {number} [groupId] Group Id to get the entry. + * @return {Promise} Resolved when done. + */ + protected setEntryIdFromOffset(dataId: number, offset?: number, groupId?: number): Promise { + if (typeof offset != 'number') { + // Entry id passed as navigation parameter instead of the offset. + // We don't display next/previous buttons in this case. + this.nextOffset = null; + this.previousOffset = null; + + return Promise.resolve(); + } + + const perPage = AddonModDataProvider.PER_PAGE; + const page = Math.floor(offset / perPage); + const pageOffset = offset % perPage; + + return this.dataProvider.getEntries(dataId, groupId, undefined, undefined, page, perPage).then((entries) => { + if (!entries || !entries.entries || !entries.entries.length || pageOffset >= entries.entries.length) { + return Promise.reject(null); + } + + this.entryId = entries.entries[pageOffset].id; + this.previousOffset = offset > 0 ? offset - 1 : null; + if (pageOffset + 1 < entries.entries.length) { + // Not the last entry on the page; + this.nextOffset = offset + 1; + } else if (entries.entries.length < perPage) { + // Last entry of the last page. + this.nextOffset = null; + } else { + // Last entry of the page, check if there are more pages. + return this.dataProvider.getEntries(dataId, groupId, undefined, undefined, page + 1, perPage).then((entries) => { + this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? offset + 1 : null; + }); + } }); } /** - * Convenience function to translate page number to entry identifier. - * - * @param {number} dataId Data Id. - * @param {number} [pageNumber] Page number where to go - * @param {number} group Group Id to get the entry. - * @return {Promise} Resolved when done. + * Function called when rating is updated online. */ - protected setEntryIdFromPage(dataId: number, pageNumber?: number, group?: number): Promise { - if (typeof pageNumber == 'number') { - return this.dataHelper.getPageInfoByPage(dataId, pageNumber, group).then((result) => { - this.entryId = result.entryId; - this.page = null; - }); - } - - return Promise.resolve(); + ratingUpdated(): void { + this.dataProvider.invalidateEntryData(this.data.id, this.entryId); } /** diff --git a/src/addon/mod/data/pages/search/search.ts b/src/addon/mod/data/pages/search/search.ts index d3eaf37a2..4ca20b5d0 100644 --- a/src/addon/mod/data/pages/search/search.ts +++ b/src/addon/mod/data/pages/search/search.ts @@ -117,6 +117,10 @@ export class AddonModDataSearchPage { [placeholder]="\'addon.mod_data.authorlastname\' | translate" formControlName="lastname">'; template = template.replace(replace, render); + // Tags are unsupported right now. + replace = new RegExp('##tags##', 'gi'); + template = template.replace(replace, ''); + return template; } diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index ca9d5da62..a50cf5156 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -18,6 +18,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModDataOfflineProvider } from './offline'; import { AddonModDataFieldsDelegate } from './fields-delegate'; @@ -35,7 +36,8 @@ export class AddonModDataProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private dataOffline: AddonModDataOfflineProvider, - private appProvider: CoreAppProvider, private fieldsDelegate: AddonModDataFieldsDelegate) { + private appProvider: CoreAppProvider, private fieldsDelegate: AddonModDataFieldsDelegate, + private logHelper: CoreCourseLogHelperProvider) { this.logger = logger.getInstance('AddonModDataProvider'); } @@ -650,10 +652,11 @@ export class AddonModDataProvider { * * @param {number} dataId Data ID for caching purposes. * @param {number} entryId Entry ID. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the database entry is retrieved. */ - getEntry(dataId: number, entryId: number, siteId?: string): Promise { + getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { entryid: entryId, @@ -663,6 +666,11 @@ export class AddonModDataProvider { cacheKey: this.getEntryCacheKey(dataId, entryId) }; + if (ignoreCache) { + preSets['getFromCache'] = false; + preSets['emergencyCache'] = false; + } + return site.read('mod_data_get_entry', params, preSets); }); } @@ -846,14 +854,15 @@ export class AddonModDataProvider { * Report the database as being viewed. * * @param {number} id Module ID. + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ - logView(id: number): Promise { + logView(id: number, siteId?: string): Promise { const params = { databaseid: id }; - return this.sitesProvider.getCurrentSite().write('mod_data_view_database', params); + return this.logHelper.log('mod_data_view_database', params, AddonModDataProvider.COMPONENT, id, siteId); } /** diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 97f6ac5e2..c713f3674 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -100,11 +100,12 @@ export class AddonModDataHelperProvider { * @param {string} template Template HMTL. * @param {any[]} fields Fields that defines every content in the entry. * @param {any} entry Entry. + * @param {number} offset Entry offset. * @param {string} mode Mode list or show. * @param {any} actions Actions that can be performed to the record. * @return {string} Generated HTML. */ - displayShowFields(template: string, fields: any[], entry: any, mode: string, actions: any): string { + displayShowFields(template: string, fields: any[], entry: any, offset: number, mode: string, actions: any): string { if (!template) { return ''; } @@ -135,7 +136,7 @@ export class AddonModDataHelperProvider { render = this.translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved')); } else { render = ''; + ']" mode="' + mode + '" [database]="data" [offset]="' + offset + '">'; } template = template.replace(replace, render); } else { @@ -172,29 +173,12 @@ export class AddonModDataHelperProvider { comments: database.comments, // Unsupported actions. + tags: false, delcheck: false, export: false }; } - /** - * Fetch all entries and return it's Id - * - * @param {number} dataId Data ID. - * @param {number} groupId Group ID. - * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. - * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). - * @param {string} [siteId] Site ID. Current if not defined. - * @return {Promise} Resolved with an array of entry ID. - */ - getAllEntriesIds(dataId: number, groupId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): - Promise { - return this.dataProvider.fetchAllEntries(dataId, groupId, undefined, undefined, undefined, forceCache, ignoreCache, siteId) - .then((entries) => { - return entries.map((entry) => entry.id); - }); - } - /** * Returns the default template of a certain type. * @@ -371,7 +355,7 @@ export class AddonModDataHelperProvider { getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise { if (entryId > 0) { // It's an online entry, get it from WS. - return this.dataProvider.getEntry(data.id, entryId, siteId); + return this.dataProvider.getEntry(data.id, entryId, false, siteId); } // It's an offline entry, search it in the offline actions. @@ -398,67 +382,6 @@ export class AddonModDataHelperProvider { }); } - /** - * Get page info related to an entry. - * - * @param {number} dataId Data ID. - * @param {number} entryId Entry ID. - * @param {number} groupId Group ID. - * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. - * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). - * @param {string} [siteId] Site ID. Current if not defined. - * @return {Promise} Containing page number, if has next and have following page. - */ - getPageInfoByEntry(dataId: number, entryId: number, groupId: number, forceCache: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - return this.getAllEntriesIds(dataId, groupId, forceCache, ignoreCache, siteId).then((entries) => { - const index = entries.findIndex((entry) => entry == entryId); - - if (index >= 0) { - return { - previousId: entries[index - 1] || false, - nextId: entries[index + 1] || false, - entryId: entryId, - page: index + 1, // Parsed to natural language. - numEntries: entries.length - }; - } - - return false; - }); - } - - /** - * Get page info related to an entry by page number. - * - * @param {number} dataId Data ID. - * @param {number} page Page number. - * @param {number} groupId Group ID. - * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. - * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). - * @param {string} [siteId] Site ID. Current if not defined. - * @return {Promise} Containing page number, if has next and have following page. - */ - getPageInfoByPage(dataId: number, page: number, groupId: number, forceCache: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - return this.getAllEntriesIds(dataId, groupId, forceCache, ignoreCache, siteId).then((entries) => { - const index = page - 1, - entryId = entries[index]; - - if (entryId) { - return { - previousId: entries[index - 1] || null, - nextId: entries[index + 1] || null, - entryId: entryId, - page: page, // Parsed to natural language. - numEntries: entries.length - }; - } - - return false; - }); - } - /** * Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles. * @@ -501,27 +424,6 @@ export class AddonModDataHelperProvider { }); } - /** - * Add a prefix to all rules in a CSS string. - * - * @param {string} css CSS code to be prefixed. - * @param {string} prefix Prefix css selector. - * @return {string} Prefixed CSS. - */ - prefixCSS(css: string, prefix: string): string { - if (!css) { - return ''; - } - - // Remove comments first. - let regExp = /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm; - css = css.replace(regExp, ''); - // Add prefix. - regExp = /([^]*?)({[^]*?}|,)/g; - - return css.replace(regExp, prefix + ' $1 $2'); - } - /** * Given a list of files (either online files or local files), store the local files in a local folder * to be submitted later. diff --git a/src/addon/mod/data/providers/module-handler.ts b/src/addon/mod/data/providers/module-handler.ts index 78cecd43b..162a9923b 100644 --- a/src/addon/mod/data/providers/module-handler.ts +++ b/src/addon/mod/data/providers/module-handler.ts @@ -67,8 +67,12 @@ export class AddonModDataModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_data-handler', showDownloadButton: true, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModDataIndexPage', {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('AddonModDataIndexPage', pageParams, options); } }; } diff --git a/src/addon/mod/data/providers/offline.ts b/src/addon/mod/data/providers/offline.ts index 76ca2556d..0c773f978 100644 --- a/src/addon/mod/data/providers/offline.ts +++ b/src/addon/mod/data/providers/offline.ts @@ -14,9 +14,11 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreFileProvider } from '@providers/file'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Service to handle Offline data. @@ -27,48 +29,67 @@ export class AddonModDataOfflineProvider { protected logger; // Variables for database. - static DATA_ENTRY_TABLE = 'addon_mod_data_entry'; - protected tablesSchema = [ - { - name: AddonModDataOfflineProvider.DATA_ENTRY_TABLE, - columns: [ - { - name: 'dataid', - type: 'INTEGER' - }, - { - name: 'courseid', - type: 'INTEGER' - }, - { - name: 'groupid', - type: 'INTEGER' - }, - { - name: 'action', - type: 'TEXT' - }, - { - name: 'entryid', - type: 'INTEGER' - }, - { - name: 'fields', - type: 'TEXT' - }, - { - name: 'timemodified', - type: 'INTEGER' - } - ], - primaryKeys: ['dataid', 'entryid'] + static DATA_ENTRY_TABLE = 'addon_mod_data_entry_1'; + protected siteSchema: CoreSiteSchema = { + name: 'AddonModDataOfflineProvider', + version: 1, + tables: [ + { + name: AddonModDataOfflineProvider.DATA_ENTRY_TABLE, + columns: [ + { + name: 'dataid', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'groupid', + type: 'INTEGER' + }, + { + name: 'action', + type: 'TEXT' + }, + { + name: 'entryid', + type: 'INTEGER' + }, + { + name: 'fields', + type: 'TEXT' + }, + { + name: 'timemodified', + type: 'INTEGER' + } + ], + primaryKeys: ['dataid', 'entryid', 'action'] + } + ], + migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise | void { + if (oldVersion == 0) { + // Move the records from the old table. + const newTable = AddonModDataOfflineProvider.DATA_ENTRY_TABLE; + const oldTable = 'addon_mod_data_entry'; + + return db.tableExists(oldTable).then(() => { + return db.insertRecordsFrom(newTable, oldTable).then(() => { + return db.dropTable(oldTable); + }); + }).catch(() => { + // Old table does not exist, ignore. + }); + } } - ]; + }; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, - private fileProvider: CoreFileProvider) { + private fileProvider: CoreFileProvider, private fileUploaderProvider: CoreFileUploaderProvider) { this.logger = logger.getInstance('AddonModDataOfflineProvider'); - this.sitesProvider.createTablesFromSchema(this.tablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); } /** @@ -102,8 +123,52 @@ export class AddonModDataOfflineProvider { */ deleteEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().deleteRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, + return this.deleteEntryFiles(dataId, entryId, action, site.id).then(() => { + return site.getDb().deleteRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, action: action}); + }); + }); + } + + /** + * Delete entry offline files. + * + * @param {number} dataId Database ID. + * @param {number} entryId Database entry ID. + * @param {string} action Action to be done. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + protected deleteEntryFiles(dataId: number, entryId: number, action: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.getEntry(dataId, entryId, action, site.id).then((entry) => { + if (!entry.fields) { + return; + } + + const promises = []; + + entry.fields.forEach((field) => { + const value = this.textUtils.parseJSON(field.value); + if (!value.offline) { + return; + } + + const promise = this.getEntryFieldFolder(dataId, entryId, field.fieldid, site.id).then((folderPath) => { + return this.fileUploaderProvider.getStoredFiles(folderPath); + }).then((files) => { + return this.fileUploaderProvider.clearTmpFiles(files); + }).catch(() => { + // Files not found, ignore. + }); + + promises.push(promise); + }); + + return Promise.all(promises); + }).catch(() => { + // Entry not found, ignore. + }); }); } diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts index b39b6b204..85c54d10e 100644 --- a/src/addon/mod/data/providers/prefetch-handler.ts +++ b/src/addon/mod/data/providers/prefetch-handler.ts @@ -24,6 +24,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; +import { CoreRatingProvider } from '@core/rating/providers/rating'; import { AddonModDataProvider } from './data'; import { AddonModDataHelperProvider } from './helper'; @@ -41,7 +42,8 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl courseProvider: CoreCourseProvider, filepoolProvider: CoreFilepoolProvider, sitesProvider: CoreSitesProvider, domUtils: CoreDomUtilsProvider, protected dataProvider: AddonModDataProvider, protected timeUtils: CoreTimeUtilsProvider, protected dataHelper: AddonModDataHelperProvider, - protected groupsProvider: CoreGroupsProvider, protected commentsProvider: CoreCommentsProvider) { + protected groupsProvider: CoreGroupsProvider, protected commentsProvider: CoreCommentsProvider, + private ratingProvider: CoreRatingProvider) { super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); } @@ -282,7 +284,11 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl }); info.entries.forEach((entry) => { - promises.push(this.dataProvider.getEntry(database.id, entry.id, siteId)); + promises.push(this.dataProvider.getEntry(database.id, entry.id, true, siteId).then((entry) => { + return this.ratingProvider.prefetchRatings('module', module.id, database.scale, courseId, entry.ratinginfo, + siteId); + })); + if (database.comments) { promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id, 'database_entry', 0, siteId)); diff --git a/src/addon/mod/data/providers/show-link-handler.ts b/src/addon/mod/data/providers/show-link-handler.ts index 1d30f33ea..3685df23e 100644 --- a/src/addon/mod/data/providers/show-link-handler.ts +++ b/src/addon/mod/data/providers/show-link-handler.ts @@ -65,7 +65,7 @@ export class AddonModDataShowLinkHandler extends CoreContentLinksHandlerBase { } if (params.mode && params.mode == 'single') { - pageParams['page'] = page || 1; + pageParams['offset'] = page || 0; } else if (rId) { pageParams['entryId'] = rId; } diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts index 6e2a90128..aeb16b190 100644 --- a/src/addon/mod/data/providers/sync.ts +++ b/src/addon/mod/data/providers/sync.ts @@ -26,7 +26,9 @@ import { AddonModDataHelperProvider } from './helper'; import { CoreEventsProvider } from '@providers/events'; import { TranslateService } from '@ngx-translate/core'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreSyncProvider } from '@providers/sync'; +import { CoreRatingSyncProvider } from '@core/rating/providers/sync'; /** * Service to sync databases. @@ -42,7 +44,8 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { private eventsProvider: CoreEventsProvider, private dataProvider: AddonModDataProvider, protected translate: TranslateService, private utils: CoreUtilsProvider, courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, timeUtils: CoreTimeUtilsProvider, - private dataHelper: AddonModDataHelperProvider) { + private dataHelper: AddonModDataHelperProvider, private logHelper: CoreCourseLogHelperProvider, + private ratingSync: CoreRatingSyncProvider) { super('AddonModDataSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); @@ -76,8 +79,12 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { * @param {Promise} Promise resolved if sync is successful, rejected if sync fails. */ protected syncAllDatabasesFunc(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + // Get all data answers pending to be sent in the site. - return this.dataOffline.getAllEntries(siteId).then((offlineActions) => { + promises.push(this.dataOffline.getAllEntries(siteId).then((offlineActions) => { const promises = {}; // Do not sync same database twice. @@ -100,7 +107,11 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { // Promises will be an object so, convert to an array first; return Promise.all(this.utils.objectToArray(promises)); - }); + })); + + promises.push(this.syncRatings(undefined, siteId)); + + return Promise.all(promises); } /** @@ -149,10 +160,15 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { updated: false }; - // Get answers to be sent. - const syncPromise = this.dataOffline.getDatabaseEntries(dataId, siteId).catch(() => { - // No offline data found, return empty object. - return []; + // Sync offline logs. + const syncPromise = this.logHelper.syncIfNeeded(AddonModDataProvider.COMPONENT, dataId, siteId).catch(() => { + // Ignore errors. + }).then(() => { + // Get answers to be sent. + return this.dataOffline.getDatabaseEntries(dataId, siteId).catch(() => { + // No offline data found, return empty object. + return []; + }); }).then((offlineActions) => { if (!offlineActions.length) { // Nothing to sync. @@ -226,7 +242,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { entryId = entryActions[0].entryid; if (entryId > 0) { - timePromise = this.dataProvider.getEntry(data.id, entryId, siteId).then((entry) => { + timePromise = this.dataProvider.getEntry(data.id, entryId, false, siteId).then((entry) => { return entry.entry.timemodified; }).catch(() => { return -1; @@ -337,4 +353,52 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { }); } + /** + * Synchronize offline ratings. + * + * @param {number} [cmId] Course module to be synced. If not defined, sync all databases. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncRatings(cmId?: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.ratingSync.syncRatings('mod_data', 'entry', 'module', cmId, 0, siteId).then((results) => { + let updated = false; + const warnings = []; + const promises = []; + + results.forEach((result) => { + promises.push(this.dataProvider.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, siteId) + .then((data) => { + const promises = []; + + if (result.updated.length) { + updated = true; + + // Invalidate entry of updated ratings. + result.updated.forEach((itemId) => { + promises.push(this.dataProvider.invalidateEntryData(data.id, itemId, siteId)); + }); + } + + if (result.warnings.length) { + result.warnings.forEach((warning) => { + warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: data.name, + error: warning + })); + }); + } + + return this.utils.allPromises(promises); + })); + }); + + return Promise.all(promises).then(() => { + return { updated, warnings }; + }); + }); + } } diff --git a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html index 10df6623c..9fef582e3 100644 --- a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html @@ -3,6 +3,7 @@ + diff --git a/src/addon/mod/feedback/components/index/index.ts b/src/addon/mod/feedback/components/index/index.ts index 1eb116187..55ffc4d21 100644 --- a/src/addon/mod/feedback/components/index/index.ts +++ b/src/addon/mod/feedback/components/index/index.ts @@ -36,6 +36,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity @Input() tab = 'overview'; @Input() group = 0; + component = AddonModFeedbackProvider.COMPONENT; moduleName = 'feedback'; access = { @@ -67,25 +68,40 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity firstSelectedTab: number; protected submitObserver: any; + protected syncEventName = AddonModFeedbackSyncProvider.AUTO_SYNCED; constructor(injector: Injector, private feedbackProvider: AddonModFeedbackProvider, @Optional() content: Content, private feedbackOffline: AddonModFeedbackOfflineProvider, private groupsProvider: CoreGroupsProvider, - private feedbackSync: AddonModFeedbackSyncProvider, private navCtrl: NavController, + private feedbackSync: AddonModFeedbackSyncProvider, protected navCtrl: NavController, private feedbackHelper: AddonModFeedbackHelperProvider, private timeUtils: CoreTimeUtilsProvider) { super(injector, content); // Listen for form submit events. this.submitObserver = this.eventsProvider.on(AddonModFeedbackProvider.FORM_SUBMITTED, (data) => { if (this.feedback && data.feedbackId == this.feedback.id) { - // Go to review attempt if an attempt in this quiz was finished and synced. this.tabsLoaded['analysis'] = false; this.tabsLoaded['overview'] = false; this.loaded = false; - if (data.tab != this.tab) { - this.tabChanged(data.tab); + + let promise; + + // Prefetch data if needed. + if (!data.offline && this.isPrefetched()) { + promise = this.feedbackSync.prefetchAfterUpdate(this.module, this.courseId).catch(() => { + // Ignore errors. + }); } else { - this.loadContent(true); + promise = Promise.resolve(); } + + promise.then(() => { + // Load the right tab. + if (data.tab != this.tab) { + this.tabChanged(data.tab); + } else { + this.loadContent(true); + } + }); } }, this.siteId); } diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts index 214f12bd7..0d8bbf9fa 100644 --- a/src/addon/mod/feedback/pages/form/form.ts +++ b/src/addon/mod/feedback/pages/form/form.ts @@ -338,8 +338,13 @@ export class AddonModFeedbackFormPage implements OnDestroy { ngOnDestroy(): void { if (this.submitted) { const tab = this.submitted == 'analysis' ? 'analysis' : 'overview'; + // If form has been submitted, the info has been already invalidated but we should update index view. - this.eventsProvider.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, {feedbackId: this.feedback.id, tab: tab}); + this.eventsProvider.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, { + feedbackId: this.feedback.id, + tab: tab, + offline: this.completedOffline + }); } this.onlineObserver && this.onlineObserver.unsubscribe(); } diff --git a/src/addon/mod/feedback/providers/feedback.ts b/src/addon/mod/feedback/providers/feedback.ts index 3fcb0099e..14976deb3 100644 --- a/src/addon/mod/feedback/providers/feedback.ts +++ b/src/addon/mod/feedback/providers/feedback.ts @@ -18,7 +18,9 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreAppProvider } from '@providers/app'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModFeedbackOfflineProvider } from './offline'; +import { CoreSiteWSPreSets } from '@classes/site'; /** * Service that provides some features for feedbacks. @@ -38,7 +40,7 @@ export class AddonModFeedbackProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private feedbackOffline: AddonModFeedbackOfflineProvider, - private appProvider: CoreAppProvider) { + private appProvider: CoreAppProvider, private logHelper: CoreCourseLogHelperProvider) { this.logger = logger.getInstance('AddonModFeedbackProvider'); } @@ -135,18 +137,6 @@ export class AddonModFeedbackProvider { */ protected fillValues(feedbackId: number, items: any[], offline: boolean, ignoreCache: boolean, siteId: string): Promise { return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId).then((valuesArray) => { - if (valuesArray.length == 0) { - // Try sending empty values to get the last completed attempt values. - return this.processPageOnline(feedbackId, 0, {}, undefined, siteId).then(() => { - return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId); - }).catch(() => { - // Ignore errors - }); - } - - return valuesArray; - - }).then((valuesArray) => { const values = {}; valuesArray.forEach((value) => { @@ -214,11 +204,14 @@ export class AddonModFeedbackProvider { * * @param {number} feedbackId Feedback ID. * @param {number} groupId Group id, 0 means that the function will determine the user group. + * @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. * @param {any} [previous] Only for recurrent use. Object with the previous fetched info. * @return {Promise} Promise resolved when the info is retrieved. */ - getAllNonRespondents(feedbackId: number, groupId: number, siteId?: string, previous?: any): Promise { + getAllNonRespondents(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) + : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (typeof previous == 'undefined') { previous = { @@ -227,7 +220,7 @@ export class AddonModFeedbackProvider { }; } - return this.getNonRespondents(feedbackId, groupId, previous.page, siteId).then((response) => { + return this.getNonRespondents(feedbackId, groupId, previous.page, ignoreCache, siteId).then((response) => { if (previous.users.length < response.total) { previous.users = previous.users.concat(response.users); } @@ -236,7 +229,7 @@ export class AddonModFeedbackProvider { // Can load more. previous.page++; - return this.getAllNonRespondents(feedbackId, groupId, siteId, previous); + return this.getAllNonRespondents(feedbackId, groupId, ignoreCache, siteId, previous); } previous.total = response.total; @@ -249,11 +242,14 @@ export class AddonModFeedbackProvider { * * @param {number} feedbackId Feedback ID. * @param {number} groupId Group id, 0 means that the function will determine the user group. + * @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. * @param {any} [previous] Only for recurrent use. Object with the previous fetched info. * @return {Promise} Promise resolved when the info is retrieved. */ - getAllResponsesAnalysis(feedbackId: number, groupId: number, siteId?: string, previous?: any): Promise { + getAllResponsesAnalysis(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) + : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (typeof previous == 'undefined') { previous = { @@ -263,7 +259,7 @@ export class AddonModFeedbackProvider { }; } - return this.getResponsesAnalysis(feedbackId, groupId, previous.page, siteId).then((responses) => { + return this.getResponsesAnalysis(feedbackId, groupId, previous.page, ignoreCache, siteId).then((responses) => { if (previous.anonattempts.length < responses.totalanonattempts) { previous.anonattempts = previous.anonattempts.concat(responses.anonattempts); } @@ -276,7 +272,7 @@ export class AddonModFeedbackProvider { // Can load more. previous.page++; - return this.getAllResponsesAnalysis(feedbackId, groupId, siteId, previous); + return this.getAllResponsesAnalysis(feedbackId, groupId, ignoreCache, siteId, previous); } previous.totalattempts = responses.totalattempts; @@ -291,15 +287,16 @@ export class AddonModFeedbackProvider { * * @param {number} feedbackId Feedback ID. * @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} Promise resolved when the feedback is retrieved. */ - getAnalysis(feedbackId: number, groupId?: number, siteId?: string): Promise { + getAnalysis(feedbackId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getAnalysisDataCacheKey(feedbackId, groupId) }; @@ -307,6 +304,11 @@ export class AddonModFeedbackProvider { params['groupid'] = groupId; } + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_analysis', params, preSets); }); } @@ -337,11 +339,12 @@ export class AddonModFeedbackProvider { * * @param {number} feedbackId Feedback ID. * @param {number} attemptId Attempt id to find. + * @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. * @param {any} [previous] Only for recurrent use. Object with the previous fetched info. * @return {Promise} Promise resolved when the info is retrieved. */ - getAttempt(feedbackId: number, attemptId: number, siteId?: string, previous?: any): Promise { + getAttempt(feedbackId: number, attemptId: number, ignoreCache?: boolean, siteId?: string, previous?: any): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (typeof previous == 'undefined') { previous = { @@ -351,7 +354,7 @@ export class AddonModFeedbackProvider { }; } - return this.getResponsesAnalysis(feedbackId, 0, previous.page, siteId).then((responses) => { + return this.getResponsesAnalysis(feedbackId, 0, previous.page, ignoreCache, siteId).then((responses) => { let attempt; attempt = responses.attempts.find((attempt) => { @@ -382,7 +385,7 @@ export class AddonModFeedbackProvider { // Can load more. Check there. previous.page++; - return this.getAttempt(feedbackId, attemptId, siteId, previous); + return this.getAttempt(feedbackId, attemptId, ignoreCache, siteId, previous); } // Not found and all loaded. Reject. @@ -404,18 +407,24 @@ export class AddonModFeedbackProvider { * Returns the temporary completion timemodified for the current user. * * @param {number} feedbackId Feedback 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} Promise resolved when the info is retrieved. */ - getCurrentCompletedTimeModified(feedbackId: number, siteId?: string): Promise { + getCurrentCompletedTimeModified(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_current_completed_tmp', params, preSets).then((response) => { if (response && typeof response.feedback != 'undefined' && typeof response.feedback.timemodified != 'undefined') { return response.feedback.timemodified; @@ -440,7 +449,7 @@ export class AddonModFeedbackProvider { } /** - * Returns the temporary completion record for the current user. + * Returns the temporary responses or responses of the last submission for the current user. * * @param {number} feedbackId Feedback ID. * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. @@ -465,11 +474,22 @@ export class AddonModFeedbackProvider { } return site.read('mod_feedback_get_unfinished_responses', params, preSets).then((response) => { - if (response && typeof response.responses != 'undefined') { - return response.responses; + if (!response || typeof response.responses == 'undefined') { + return Promise.reject(null); } - return Promise.reject(null); + if (response.responses.length == 0) { + // No unfinished responses, fetch responses of the last submission. + return site.read('mod_feedback_get_finished_responses', params, preSets).then((response) => { + if (!response || typeof response.responses == 'undefined') { + return Promise.reject(null); + } + + return response.responses; + }); + } + + return response.responses; }); }); } @@ -551,20 +571,26 @@ export class AddonModFeedbackProvider { * @param {string} key Name of the property to check. * @param {any} value Value to search. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the feedback is retrieved. */ - protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean): Promise { + protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean, + ignoreCache?: boolean): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getFeedbackCacheKey(courseId) }; if (forceCache) { - preSets['omitExpires'] = true; + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; } return site.read('mod_feedback_get_feedbacks_by_courses', params, preSets).then((response) => { @@ -588,11 +614,12 @@ export class AddonModFeedbackProvider { * @param {number} courseId Course ID. * @param {number} cmId Course module ID. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the feedback is retrieved. */ - getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean): Promise { - return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache); + getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache, ignoreCache); } /** @@ -602,28 +629,35 @@ export class AddonModFeedbackProvider { * @param {number} id Feedback ID. * @param {string} [siteId] Site ID. If not defined, current site. * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the feedback is retrieved. */ - getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean): Promise { - return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache); + getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache, ignoreCache); } /** * Returns the items (questions) in the given feedback. * * @param {number} feedbackId Feedback 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} Promise resolved when the info is retrieved. */ - getItems(feedbackId: number, siteId?: string): Promise { + getItems(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getItemsDataCacheKey(feedbackId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_items', params, preSets); }); } @@ -644,20 +678,28 @@ export class AddonModFeedbackProvider { * @param {number} feedbackId Feedback ID. * @param {number} [groupId=0] Group id, 0 means that the function will determine the user group. * @param {number} [page=0] The page of records to return. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the info is retrieved. */ - getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, siteId?: string): Promise { + getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, ignoreCache?: boolean, siteId?: string) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId, groupid: groupId, page: page }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, groupId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_non_respondents', params, preSets); }); } @@ -724,7 +766,7 @@ export class AddonModFeedbackProvider { }); }).catch(() => { // If getPageItems fail we should calculate it using getItems. - return this.getItems(feedbackId, siteId).then((response) => { + return this.getItems(feedbackId, false, siteId).then((response) => { return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { // Separate items by pages. let currentPage = 0; @@ -801,20 +843,26 @@ export class AddonModFeedbackProvider { * @param {number} feedbackId Feedback ID. * @param {number} groupId Group id, 0 means that the function will determine the user group. * @param {number} page The page of records to return. + * @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} Promise resolved when the info is retrieved. */ - getResponsesAnalysis(feedbackId: number, groupId: number, page: number, siteId?: string): Promise { + getResponsesAnalysis(feedbackId: number, groupId: number, page: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId, groupid: groupId || 0, page: page || 0 }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, groupId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_responses_analysis', params, preSets); }); } @@ -1036,18 +1084,24 @@ export class AddonModFeedbackProvider { * Returns if feedback has been completed * * @param {number} feedbackId Feedback ID. + * @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} Promise resolved when the info is retrieved. */ - isCompleted(feedbackId: number, siteId?: string): Promise { + isCompleted(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getCompletedDataCacheKey(feedbackId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return this.utils.promiseWorks(site.read('mod_feedback_get_last_completed', params, preSets)); }); } @@ -1071,15 +1125,16 @@ export class AddonModFeedbackProvider { * * @param {number} id Module ID. * @param {boolean} [formViewed=false] True if form was viewed. + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ - logView(id: number, formViewed: boolean = false): Promise { + logView(id: number, formViewed: boolean = false, siteId?: string): Promise { const params = { feedbackid: id, moduleviewed: formViewed ? 1 : 0 }; - return this.sitesProvider.getCurrentSite().write('mod_feedback_view_feedback', params); + return this.logHelper.log('mod_feedback_view_feedback', params, AddonModFeedbackProvider.COMPONENT, id, siteId); } /** diff --git a/src/addon/mod/feedback/providers/module-handler.ts b/src/addon/mod/feedback/providers/module-handler.ts index 8da7e41d9..7744ce1b3 100644 --- a/src/addon/mod/feedback/providers/module-handler.ts +++ b/src/addon/mod/feedback/providers/module-handler.ts @@ -65,8 +65,12 @@ export class AddonModFeedbackModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_feedback-handler', showDownloadButton: true, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModFeedbackIndexPage', {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('AddonModFeedbackIndexPage', pageParams, options); } }; } diff --git a/src/addon/mod/feedback/providers/offline.ts b/src/addon/mod/feedback/providers/offline.ts index d8c26f1c4..b9fd588ef 100644 --- a/src/addon/mod/feedback/providers/offline.ts +++ b/src/addon/mod/feedback/providers/offline.ts @@ -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 { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -28,39 +28,43 @@ export class AddonModFeedbackOfflineProvider { // Variables for database. static FEEDBACK_TABLE = 'addon_mod_feedback_answers'; - protected tablesSchema = [ - { - name: AddonModFeedbackOfflineProvider.FEEDBACK_TABLE, - columns: [ - { - name: 'feedbackid', - type: 'INTEGER' - }, - { - name: 'page', - type: 'INTEGER' - }, - { - name: 'courseid', - type: 'INTEGER' - }, - { - name: 'responses', - type: 'TEXT' - }, - { - name: 'timemodified', - type: 'INTEGER' - } - ], - primaryKeys: ['feedbackid', 'page'] - } - ]; + protected siteSchema: CoreSiteSchema = { + name: 'AddonModFeedbackOfflineProvider', + version: 1, + tables: [ + { + name: AddonModFeedbackOfflineProvider.FEEDBACK_TABLE, + columns: [ + { + name: 'feedbackid', + type: 'INTEGER' + }, + { + name: 'page', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'responses', + type: 'TEXT' + }, + { + name: 'timemodified', + type: 'INTEGER' + } + ], + primaryKeys: ['feedbackid', 'page'] + } + ] + }; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { this.logger = logger.getInstance('AddonModFeedbackOfflineProvider'); - this.sitesProvider.createTablesFromSchema(this.tablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); } /** diff --git a/src/addon/mod/feedback/providers/prefetch-handler.ts b/src/addon/mod/feedback/providers/prefetch-handler.ts index 73a9dfc6b..ab9528fc7 100644 --- a/src/addon/mod/feedback/providers/prefetch-handler.ts +++ b/src/addon/mod/feedback/providers/prefetch-handler.ts @@ -173,19 +173,15 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH */ protected prefetchFeedback(module: any, courseId: number, single: boolean, siteId: string): Promise { // Prefetch the feedback data. - return this.feedbackProvider.getFeedback(courseId, module.id).then((feedback) => { - const p1 = []; + return this.feedbackProvider.getFeedback(courseId, module.id, siteId, false, true).then((feedback) => { + let files = (feedback.pageaftersubmitfiles || []).concat(this.getIntroFilesFromInstance(module, feedback)); - p1.push(this.getFiles(module, courseId).then((files) => { - return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); - })); - - p1.push(this.feedbackProvider.getFeedbackAccessInformation(feedback.id, false, true, siteId).then((accessData) => { + return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, false, true, siteId).then((accessData) => { const p2 = []; if (accessData.canedititems || accessData.canviewreports) { // Get all groups analysis. - p2.push(this.feedbackProvider.getAnalysis(feedback.id, undefined, siteId)); - p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId) + p2.push(this.feedbackProvider.getAnalysis(feedback.id, undefined, true, siteId)); + p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId, true) .then((groupInfo) => { const p3 = [], userIds = []; @@ -194,8 +190,8 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH groupInfo.groups = [{id: 0}]; } groupInfo.groups.forEach((group) => { - p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, siteId)); - p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, siteId) + p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, true, siteId)); + p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, true, siteId) .then((responses) => { responses.attempts.forEach((attempt) => { userIds.push(attempt.userid); @@ -203,7 +199,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH })); if (!accessData.isanonymous) { - p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, group.id, siteId) + p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, group.id, true, siteId) .then((responses) => { responses.users.forEach((user) => { userIds.push(user.userid); @@ -219,7 +215,13 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH })); } - p2.push(this.feedbackProvider.getItems(feedback.id, siteId)); + p2.push(this.feedbackProvider.getItems(feedback.id, true, siteId).then((response) => { + response.items.forEach((item) => { + files = files.concat(item.itemfiles); + }); + + return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); + })); if (accessData.cancomplete && accessData.cansubmit && !accessData.isempty) { // Send empty data, so it will recover last completed feedback attempt values. @@ -234,9 +236,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH } return Promise.all(p2); - })); - - return Promise.all(p1); + }); }); } } diff --git a/src/addon/mod/feedback/providers/show-entries-link-handler.ts b/src/addon/mod/feedback/providers/show-entries-link-handler.ts index fd32a42fe..d10c156da 100644 --- a/src/addon/mod/feedback/providers/show-entries-link-handler.ts +++ b/src/addon/mod/feedback/providers/show-entries-link-handler.ts @@ -65,7 +65,7 @@ export class AddonModFeedbackShowEntriesLinkHandler extends CoreContentLinksHand return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackRespondentsPage', stateParams, siteId); } - return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, siteId).then((attempt) => { + return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, true, siteId).then((attempt) => { stateParams = { moduleId: module.id, attempt: attempt, diff --git a/src/addon/mod/feedback/providers/sync.ts b/src/addon/mod/feedback/providers/sync.ts index 4aca43570..6c6e008a4 100644 --- a/src/addon/mod/feedback/providers/sync.ts +++ b/src/addon/mod/feedback/providers/sync.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -25,13 +24,17 @@ import { AddonModFeedbackProvider } from './feedback'; import { CoreEventsProvider } from '@providers/events'; import { TranslateService } from '@ngx-translate/core'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreSyncProvider } from '@providers/sync'; +import { AddonModFeedbackPrefetchHandler } from './prefetch-handler'; /** * Service to sync feedbacks. */ @Injectable() -export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { +export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_feedback_autom_synced'; protected componentTranslate: string; @@ -40,13 +43,31 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { protected appProvider: CoreAppProvider, private feedbackOffline: AddonModFeedbackOfflineProvider, private eventsProvider: CoreEventsProvider, private feedbackProvider: AddonModFeedbackProvider, protected translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, - courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider, timeUtils: CoreTimeUtilsProvider) { + private courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider, timeUtils: CoreTimeUtilsProvider, + private logHelper: CoreCourseLogHelperProvider, prefetchDelegate: CoreCourseModulePrefetchDelegate, + prefetchHandler: AddonModFeedbackPrefetchHandler) { + super('AddonModFeedbackSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('feedback'); } + /** + * Conveniece function to prefetch data after an update. + * + * @param {any} module Module. + * @param {number} courseId Course ID. + * @param {RegExp} [regex] If regex matches, don't download the data. Defaults to check files and timers. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchAfterUpdate(module: any, courseId: number, regex?: RegExp, siteId?: string): Promise { + regex = regex || /^.*files$|^timers/; + + return super.prefetchAfterUpdate(module, courseId, regex, siteId); + } + /** * Try to synchronize all the feedbacks in a certain site or in all sites. * @@ -144,10 +165,15 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { this.logger.debug(`Try to sync feedback '${feedbackId}' in site ${siteId}'`); - // Get offline responses to be sent. - const syncPromise = this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).catch(() => { - // No offline data found, return empty array. - return []; + // Sync offline logs. + const syncPromise = this.logHelper.syncIfNeeded(AddonModFeedbackProvider.COMPONENT, feedbackId, siteId).catch(() => { + // Ignore errors. + }).then(() => { + // Get offline responses to be sent. + return this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + }); }).then((responses) => { if (!responses.length) { // Nothing to sync. @@ -189,7 +215,7 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { return Promise.all(promises); } - return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, siteId).then((timemodified) => { + return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, true, siteId).then((timemodified) => { // Sort by page. responses.sort((a, b) => { return a.page - b.page; @@ -209,8 +235,10 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { }); }).then(() => { if (result.updated) { - // Data has been sent to server. Now invalidate the WS calls. - return this.feedbackProvider.invalidateAllFeedbackData(feedbackId, siteId).catch(() => { + // Data has been sent to server, update data. + return this.courseProvider.getModuleBasicInfoByInstance(feedbackId, 'feedback', siteId).then((module) => { + return this.prefetchAfterUpdate(module, courseId, undefined, siteId); + }).catch(() => { // Ignore errors. }); } diff --git a/src/addon/mod/folder/components/index/addon-mod-folder-index.html b/src/addon/mod/folder/components/index/addon-mod-folder-index.html index 48bc258e1..62cda2e8e 100644 --- a/src/addon/mod/folder/components/index/addon-mod-folder-index.html +++ b/src/addon/mod/folder/components/index/addon-mod-folder-index.html @@ -3,6 +3,7 @@ + @@ -17,8 +18,8 @@ - -

{{file.name}}

+ +

{{file.name}}

diff --git a/src/addon/mod/folder/providers/folder.ts b/src/addon/mod/folder/providers/folder.ts index 63c5767bd..f26369af8 100644 --- a/src/addon/mod/folder/providers/folder.ts +++ b/src/addon/mod/folder/providers/folder.ts @@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; /** * Service that provides some features for folder. @@ -29,7 +30,7 @@ export class AddonModFolderProvider { protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, - private utils: CoreUtilsProvider) { + private utils: CoreUtilsProvider, private logHelper: CoreCourseLogHelperProvider) { this.logger = logger.getInstance('AddonModFolderProvider'); } @@ -132,13 +133,14 @@ export class AddonModFolderProvider { * Report a folder as being viewed. * * @param {number} id Module ID. + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ - logView(id: number): Promise { + logView(id: number, siteId?: string): Promise { const params = { folderid: id }; - return this.sitesProvider.getCurrentSite().write('mod_folder_view_folder', params); + return this.logHelper.log('mod_folder_view_folder', params, AddonModFolderProvider.COMPONENT, id, siteId); } } diff --git a/src/addon/mod/folder/providers/module-handler.ts b/src/addon/mod/folder/providers/module-handler.ts index c17f6f0a2..5d372a12c 100644 --- a/src/addon/mod/folder/providers/module-handler.ts +++ b/src/addon/mod/folder/providers/module-handler.ts @@ -64,8 +64,12 @@ export class AddonModFolderModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_folder-handler', showDownloadButton: true, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModFolderIndexPage', {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('AddonModFolderIndexPage', pageParams, options); } }; } diff --git a/src/addon/mod/forum/components/components.module.ts b/src/addon/mod/forum/components/components.module.ts index e9b656899..0f3bf1b10 100644 --- a/src/addon/mod/forum/components/components.module.ts +++ b/src/addon/mod/forum/components/components.module.ts @@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { CoreRatingComponentsModule } from '@core/rating/components/components.module'; import { AddonModForumIndexComponent } from './index/index'; import { AddonModForumPostComponent } from './post/post'; @@ -35,7 +36,8 @@ import { AddonModForumPostComponent } from './post/post'; CoreComponentsModule, CoreDirectivesModule, CorePipesModule, - CoreCourseComponentsModule + CoreCourseComponentsModule, + CoreRatingComponentsModule ], providers: [ ], diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html index bcbce5865..47b6037c1 100644 --- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html +++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html @@ -3,8 +3,9 @@ - - + + + @@ -20,7 +21,7 @@ - + {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} diff --git a/src/addon/mod/forum/components/index/index.ts b/src/addon/mod/forum/components/index/index.ts index 774c8ae4d..036697410 100644 --- a/src/addon/mod/forum/components/index/index.ts +++ b/src/addon/mod/forum/components/index/index.ts @@ -19,6 +19,9 @@ import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreGroupsProvider } from '@providers/groups'; +import { CoreRatingProvider } from '@core/rating/providers/rating'; +import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; +import { CoreRatingSyncProvider } from '@core/rating/providers/sync'; import { AddonModForumProvider } from '../../providers/forum'; import { AddonModForumHelperProvider } from '../../providers/helper'; import { AddonModForumOfflineProvider } from '../../providers/offline'; @@ -56,6 +59,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom protected newDiscObserver: any; protected viewDiscObserver: any; + hasOfflineRatings: boolean; + protected ratingOfflineObserver: any; + protected ratingSyncObserver: any; + constructor(injector: Injector, @Optional() protected content: Content, protected navCtrl: NavController, @@ -66,7 +73,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom protected forumOffline: AddonModForumOfflineProvider, protected forumSync: AddonModForumSyncProvider, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, - protected prefetchHandler: AddonModForumPrefetchHandler) { + protected prefetchHandler: AddonModForumPrefetchHandler, + protected ratingOffline: CoreRatingOfflineProvider) { super(injector); } @@ -100,6 +108,22 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } }, this.sitesProvider.getCurrentSiteId()); + // Listen for offline ratings saved and synced. + this.ratingOfflineObserver = this.eventsProvider.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => { + if (this.forum && data.component == 'mod_forum' && data.ratingArea == 'post' && + data.contextLevel == 'module' && data.instanceId == this.forum.cmid) { + this.hasOfflineRatings = true; + } + }); + this.ratingSyncObserver = this.eventsProvider.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => { + if (this.forum && data.component == 'mod_forum' && data.ratingArea == 'post' && + data.contextLevel == 'module' && data.instanceId == this.forum.cmid) { + this.ratingOffline.hasRatings('mod_forum', 'post', 'module', this.forum.cmid).then((hasRatings) => { + this.hasOfflineRatings = hasRatings; + }); + } + }); + this.loadContent(false, true).then(() => { if (!this.forum) { return; @@ -178,6 +202,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom return Promise.all([ this.fetchOfflineDiscussion(), this.fetchDiscussions(refresh), + this.ratingOffline.hasRatings('mod_forum', 'post', 'module', this.forum.cmid).then((hasRatings) => { + this.hasOfflineRatings = hasRatings; + }) ]); }).catch((message) => { if (!refresh) { @@ -351,21 +378,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom protected sync(): Promise { const promises = []; - promises.push(this.forumSync.syncForumDiscussions(this.forum.id).then((result) => { - if (result.warnings && result.warnings.length) { - this.domUtils.showErrorModal(result.warnings[0]); - } - - return result; - })); - - promises.push(this.forumSync.syncForumReplies(this.forum.id).then((result) => { - if (result.warnings && result.warnings.length) { - this.domUtils.showErrorModal(result.warnings[0]); - } - - return result; - })); + promises.push(this.forumSync.syncForumDiscussions(this.forum.id)); + promises.push(this.forumSync.syncForumReplies(this.forum.id)); + promises.push(this.forumSync.syncRatings(this.forum.cmid)); return Promise.all(promises).then((results) => { return results.reduce((a, b) => ({ @@ -476,5 +491,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.newDiscObserver && this.newDiscObserver.off(); this.replyObserver && this.replyObserver.off(); this.viewDiscObserver && this.viewDiscObserver.off(); + this.ratingOfflineObserver && this.ratingOfflineObserver.off(); + this.ratingSyncObserver && this.ratingSyncObserver.off(); } } diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html index cad199ead..6592dabb2 100644 --- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html +++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html @@ -1,4 +1,4 @@ - +

@@ -23,6 +23,8 @@
+ + diff --git a/src/addon/mod/scorm/pages/player/player.ts b/src/addon/mod/scorm/pages/player/player.ts index eafadea86..49f2cd000 100644 --- a/src/addon/mod/scorm/pages/player/player.ts +++ b/src/addon/mod/scorm/pages/player/player.ts @@ -13,17 +13,17 @@ // limitations under the License. import { Component, OnInit, OnDestroy } from '@angular/core'; -import { IonicPage, NavParams, PopoverController } from 'ionic-angular'; +import { IonicPage, NavParams, ModalController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreIonTabsComponent } from '@components/ion-tabs/ion-tabs'; import { AddonModScormProvider, AddonModScormAttemptCountResult } from '../../providers/scorm'; import { AddonModScormHelperProvider } from '../../providers/helper'; import { AddonModScormSyncProvider } from '../../providers/scorm-sync'; import { AddonModScormDataModel12 } from '../../classes/data-model-12'; -import { AddonModScormTocPopoverComponent } from '../../components/toc-popover/toc-popover'; /** * Page that allows playing a SCORM. @@ -64,11 +64,11 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { protected launchPrevObserver: any; protected goOfflineObserver: any; - constructor(navParams: NavParams, protected popoverCtrl: PopoverController, protected eventsProvider: CoreEventsProvider, + constructor(navParams: NavParams, protected modalCtrl: ModalController, protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, protected timeUtils: CoreTimeUtilsProvider, protected scormProvider: AddonModScormProvider, protected scormHelper: AddonModScormHelperProvider, - protected scormSyncProvider: AddonModScormSyncProvider) { + protected scormSyncProvider: AddonModScormSyncProvider, protected tabs: CoreIonTabsComponent) { this.scorm = navParams.get('scorm') || {}; this.mode = navParams.get('mode') || AddonModScormProvider.MODENORMAL; @@ -92,6 +92,8 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.showToc = this.scormProvider.displayTocInPlayer(this.scorm); if (this.scorm.popup) { + this.tabs.changeVisibility(false); + // If we receive a value <= 100 we need to assume it's a percentage. if (this.scorm.width <= 100) { this.scorm.width = this.scorm.width + '%'; @@ -379,20 +381,25 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { * @param {MouseEvent} event Event. */ openToc(event: MouseEvent): void { - const popover = this.popoverCtrl.create(AddonModScormTocPopoverComponent, { + const modal = this.modalCtrl.create('AddonModScormTocPage', { toc: this.toc, attemptToContinue: this.attemptToContinue, - mode: this.mode - }); + mode: this.mode, + selected: this.currentSco && this.currentSco.id + }, { cssClass: 'core-modal-lateral', + showBackdrop: true, + enableBackdropDismiss: true, + enterAnimation: 'core-modal-lateral-transition', + leaveAnimation: 'core-modal-lateral-transition' }); - // If the popover sends back a SCO, load it. - popover.onDidDismiss((sco) => { + // If the modal sends back a SCO, load it. + modal.onDidDismiss((sco) => { if (sco) { this.loadSco(sco); } }); - popover.present({ + modal.present({ ev: event }); } @@ -442,9 +449,12 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.tocObserver && this.tocObserver.off(); this.launchNextObserver && this.launchNextObserver.off(); this.launchPrevObserver && this.launchPrevObserver.off(); - this.goOfflineObserver && this.goOfflineObserver.off(); + setTimeout(() => { + this.goOfflineObserver && this.goOfflineObserver.off(); + }, 500); // Unblock the SCORM so it can be synced. this.syncProvider.unblockOperation(AddonModScormProvider.COMPONENT, this.scorm.id, 'player'); + this.tabs.changeVisibility(true); } } diff --git a/src/addon/mod/scorm/pages/toc/toc.html b/src/addon/mod/scorm/pages/toc/toc.html new file mode 100644 index 000000000..c9a0c71c4 --- /dev/null +++ b/src/addon/mod/scorm/pages/toc/toc.html @@ -0,0 +1,33 @@ + + + {{ 'addon.mod_scorm.toc' | translate }} + + + + + + + + diff --git a/src/addon/mod/scorm/pages/toc/toc.module.ts b/src/addon/mod/scorm/pages/toc/toc.module.ts new file mode 100644 index 000000000..4f7684d1a --- /dev/null +++ b/src/addon/mod/scorm/pages/toc/toc.module.ts @@ -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 { AddonModScormTocPage } from './toc'; + +@NgModule({ + declarations: [ + AddonModScormTocPage, + ], + imports: [ + CoreDirectivesModule, + IonicPageModule.forChild(AddonModScormTocPage), + TranslateModule.forChild() + ], +}) +export class AddonModScormTocPageModule {} diff --git a/src/addon/mod/scorm/components/toc-popover/toc-popover.ts b/src/addon/mod/scorm/pages/toc/toc.ts similarity index 76% rename from src/addon/mod/scorm/components/toc-popover/toc-popover.ts rename to src/addon/mod/scorm/pages/toc/toc.ts index 7a8b51fb1..036196b6c 100644 --- a/src/addon/mod/scorm/components/toc-popover/toc-popover.ts +++ b/src/addon/mod/scorm/pages/toc/toc.ts @@ -13,27 +13,30 @@ // limitations under the License. import { Component } from '@angular/core'; -import { NavParams, ViewController } from 'ionic-angular'; +import { IonicPage, NavParams, ViewController } from 'ionic-angular'; import { AddonModScormProvider } from '../../providers/scorm'; /** - * Component to display the TOC of a SCORM. + * Modal to display the TOC of a SCORM. */ +@IonicPage({ segment: 'addon-mod-scorm-toc-modal' }) @Component({ - selector: 'addon-mod-scorm-toc-popover', - templateUrl: 'addon-mod-scorm-toc-popover.html' + selector: 'page-addon-mod-scorm-toc', + templateUrl: 'toc.html' }) -export class AddonModScormTocPopoverComponent { +export class AddonModScormTocPage { toc: any[]; isBrowse: boolean; isReview: boolean; attemptToContinue: number; + selected: number; constructor(navParams: NavParams, private viewCtrl: ViewController) { this.toc = navParams.get('toc') || []; this.attemptToContinue = navParams.get('attemptToContinue'); const mode = navParams.get('mode'); + this.selected = navParams.get('selected'); this.isBrowse = mode === AddonModScormProvider.MODEBROWSE; this.isReview = mode === AddonModScormProvider.MODEREVIEW; @@ -51,4 +54,11 @@ export class AddonModScormTocPopoverComponent { this.viewCtrl.dismiss(sco); } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } } diff --git a/src/addon/mod/scorm/providers/module-handler.ts b/src/addon/mod/scorm/providers/module-handler.ts index b096f7463..1f1df4c8d 100644 --- a/src/addon/mod/scorm/providers/module-handler.ts +++ b/src/addon/mod/scorm/providers/module-handler.ts @@ -64,8 +64,12 @@ export class AddonModScormModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_scorm-handler', showDownloadButton: true, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModScormIndexPage', {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('AddonModScormIndexPage', pageParams, options); } }; } diff --git a/src/addon/mod/scorm/providers/scorm-offline.ts b/src/addon/mod/scorm/providers/scorm-offline.ts index 910628d46..74af07260 100644 --- a/src/addon/mod/scorm/providers/scorm-offline.ts +++ b/src/addon/mod/scorm/providers/scorm-offline.ts @@ -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 { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -34,95 +34,99 @@ export class AddonModScormOfflineProvider { // Variables for database. static ATTEMPTS_TABLE = 'addon_mod_scorm_offline_attempts'; static TRACKS_TABLE = 'addon_mod_scorm_offline_scos_tracks'; - protected tablesSchema = [ - { - name: AddonModScormOfflineProvider.ATTEMPTS_TABLE, - columns: [ - { - name: 'scormid', - type: 'INTEGER', - notNull: true - }, - { - name: 'attempt', // Attempt number. - type: 'INTEGER', - notNull: true - }, - { - name: 'userid', - type: 'INTEGER', - notNull: true - }, - { - name: 'courseid', - type: 'INTEGER' - }, - { - name: 'timecreated', - type: 'INTEGER' - }, - { - name: 'timemodified', - type: 'INTEGER' - }, - { - name: 'snapshot', - type: 'TEXT' - }, - ], - primaryKeys: ['scormid', 'userid', 'attempt'] - }, - { - name: AddonModScormOfflineProvider.TRACKS_TABLE, - columns: [ - { - name: 'scormid', - type: 'INTEGER', - notNull: true - }, - { - name: 'attempt', // Attempt number. - type: 'INTEGER', - notNull: true - }, - { - name: 'userid', - type: 'INTEGER', - notNull: true - }, - { - name: 'scoid', - type: 'INTEGER', - notNull: true - }, - { - name: 'element', - type: 'TEXT', - notNull: true - }, - { - name: 'value', - type: 'TEXT' - }, - { - name: 'timemodified', - type: 'INTEGER' - }, - { - name: 'synced', - type: 'INTEGER' - }, - ], - primaryKeys: ['scormid', 'userid', 'attempt', 'scoid', 'element'] - } - ]; + protected siteSchema: CoreSiteSchema = { + name: 'AddonModScormOfflineProvider', + version: 1, + tables: [ + { + name: AddonModScormOfflineProvider.ATTEMPTS_TABLE, + columns: [ + { + name: 'scormid', + type: 'INTEGER', + notNull: true + }, + { + name: 'attempt', // Attempt number. + type: 'INTEGER', + notNull: true + }, + { + name: 'userid', + type: 'INTEGER', + notNull: true + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'timecreated', + type: 'INTEGER' + }, + { + name: 'timemodified', + type: 'INTEGER' + }, + { + name: 'snapshot', + type: 'TEXT' + }, + ], + primaryKeys: ['scormid', 'userid', 'attempt'] + }, + { + name: AddonModScormOfflineProvider.TRACKS_TABLE, + columns: [ + { + name: 'scormid', + type: 'INTEGER', + notNull: true + }, + { + name: 'attempt', // Attempt number. + type: 'INTEGER', + notNull: true + }, + { + name: 'userid', + type: 'INTEGER', + notNull: true + }, + { + name: 'scoid', + type: 'INTEGER', + notNull: true + }, + { + name: 'element', + type: 'TEXT', + notNull: true + }, + { + name: 'value', + type: 'TEXT' + }, + { + name: 'timemodified', + type: 'INTEGER' + }, + { + name: 'synced', + type: 'INTEGER' + }, + ], + primaryKeys: ['scormid', 'userid', 'attempt', 'scoid', 'element'] + } + ] + }; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider, private syncProvider: CoreSyncProvider, private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, private userProvider: CoreUserProvider) { this.logger = logger.getInstance('AddonModScormOfflineProvider'); - this.sitesProvider.createTablesFromSchema(this.tablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); } /** @@ -430,6 +434,8 @@ export class AddonModScormOfflineProvider { * @return {{[scoId: number]: string}} Launch URLs indexed by SCO ID. */ protected getLaunchUrlsFromScos(scos: any[]): {[scoId: number]: string} { + scos = scos || []; + const response = {}; scos.forEach((sco) => { @@ -487,12 +493,15 @@ export class AddonModScormOfflineProvider { * * @param {number} scormId SCORM ID. * @param {number} attempt Attempt number. - * @param {any[]} scos SCOs returned by AddonModScormProvider.getScos. + * @param {any[]} scos SCOs returned by AddonModScormProvider.getScos. If not supplied, this function will only return the + * SCOs that have something stored and cmi.launch_data will be undefined. * @param {string} [siteId] Site ID. If not defined, current site. * @param {number} [userId] User ID. If not defined use site's current user. * @return {Promise} Promise resolved when the user data is retrieved. */ getScormUserData(scormId: number, attempt: number, scos: any[], siteId?: string, userId?: number): Promise { + scos = scos || []; + let fullName = '', userName = ''; diff --git a/src/addon/mod/scorm/providers/scorm-sync.ts b/src/addon/mod/scorm/providers/scorm-sync.ts index 9a5d06ae7..683cfd450 100644 --- a/src/addon/mod/scorm/providers/scorm-sync.ts +++ b/src/addon/mod/scorm/providers/scorm-sync.ts @@ -23,7 +23,9 @@ 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 { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm'; import { AddonModScormOfflineProvider } from './scorm-offline'; import { AddonModScormPrefetchHandler } from './prefetch-handler'; @@ -55,7 +57,7 @@ export interface AddonModScormSyncResult { * Service to sync SCORMs. */ @Injectable() -export class AddonModScormSyncProvider extends CoreSyncBaseProvider { +export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_scorm_autom_synced'; @@ -63,12 +65,14 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, - courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, timeUtils: CoreTimeUtilsProvider, + private eventsProvider: CoreEventsProvider, timeUtils: CoreTimeUtilsProvider, private scormProvider: AddonModScormProvider, private scormOfflineProvider: AddonModScormOfflineProvider, - private prefetchHandler: AddonModScormPrefetchHandler, private utils: CoreUtilsProvider) { + prefetchHandler: AddonModScormPrefetchHandler, private utils: CoreUtilsProvider, + prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseProvider: CoreCourseProvider, + private logHelper: CoreCourseLogHelperProvider) { super('AddonModScormSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('scorm'); } @@ -190,11 +194,11 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { let promise; if (updated) { - // Update the WS data. - promise = this.scormProvider.invalidateAllScormData(scorm.id, siteId).catch(() => { + // Update downloaded data. + promise = this.courseProvider.getModuleBasicInfoByInstance(scorm.id, 'scorm', siteId).then((module) => { + return this.prefetchAfterUpdate(module, scorm.course, undefined, siteId); + }).catch(() => { // Ignore errors. - }).then(() => { - return this.prefetchHandler.fetchWSData(scorm, siteId); }); } else { promise = Promise.resolve(); @@ -619,8 +623,13 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { this.logger.debug('Try to sync SCORM ' + scorm.id + ' in site ' + siteId); - // Get attempts data. We ignore cache for online attempts, so this call will fail if offline or server down. - syncPromise = this.scormProvider.getAttemptCount(scorm.id, false, true, siteId).then((attemptsData) => { + // Sync offline logs. + syncPromise = this.logHelper.syncIfNeeded(AddonModScormProvider.COMPONENT, scorm.id, siteId).catch(() => { + // Ignore errors. + }).then(() => { + // Get attempts data. We ignore cache for online attempts, so this call will fail if offline or server down. + return this.scormProvider.getAttemptCount(scorm.id, false, true, siteId); + }).then((attemptsData) => { if (!attemptsData.offline || !attemptsData.offline.length) { // Nothing to sync. return this.finishSync(siteId, scorm, warnings, lastOnline, lastOnlineWasFinished); diff --git a/src/addon/mod/scorm/providers/scorm.ts b/src/addon/mod/scorm/providers/scorm.ts index 6c3a7a151..0ee62a7f7 100644 --- a/src/addon/mod/scorm/providers/scorm.ts +++ b/src/addon/mod/scorm/providers/scorm.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; @@ -25,6 +26,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonModScormOfflineProvider } from './scorm-offline'; import { CoreSiteWSPreSets } from '@classes/site'; import { CoreConstants } from '@core/constants'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; /** * Result of getAttemptCount. @@ -86,6 +88,7 @@ export class AddonModScormProvider { static LAUNCH_PREV_SCO_EVENT = 'addon_mod_scorm_launch_prev_sco'; static UPDATE_TOC_EVENT = 'addon_mod_scorm_update_toc'; static GO_OFFLINE_EVENT = 'addon_mod_scorm_go_offline'; + static DATA_SENT_EVENT = 'addon_mod_scorm_data_sent'; // Protected constants. protected VALID_STATUSES = ['notattempted', 'passed', 'completed', 'failed', 'incomplete', 'browsed', 'suspend']; @@ -110,7 +113,8 @@ export class AddonModScormProvider { constructor(logger: CoreLoggerProvider, private translate: TranslateService, private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private scormOfflineProvider: AddonModScormOfflineProvider, - private timeUtils: CoreTimeUtilsProvider, private syncProvider: CoreSyncProvider) { + private timeUtils: CoreTimeUtilsProvider, private syncProvider: CoreSyncProvider, + private eventsProvider: CoreEventsProvider, private logHelper: CoreCourseLogHelperProvider) { this.logger = logger.getInstance('AddonModScormProvider'); } @@ -1443,18 +1447,12 @@ export class AddonModScormProvider { * @return {Promise} Promise resolved when the WS call is successful. */ logView(id: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - scormid: id - }; + const params = { + scormid: id + }; - return site.write('mod_scorm_view_scorm', params).then((response) => { - if (!response || !response.status) { - return Promise.reject(null); - } - }); - }); - } + return this.logHelper.log('mod_scorm_view_scorm', params, AddonModScormProvider.COMPONENT, id, siteId); +} /** * Saves a SCORM tracking record. @@ -1483,6 +1481,12 @@ export class AddonModScormProvider { return this.saveTracksOnline(scorm.id, scoId, attempt, tracks, siteId).then(() => { // Tracks have been saved, update cached user data. this.updateUserDataAfterSave(scorm.id, attempt, tracks, siteId); + + this.eventsProvider.trigger(AddonModScormProvider.DATA_SENT_EVENT, { + scormId: scorm.id, + scoId: scoId, + attempt: attempt + }, this.sitesProvider.getCurrentSiteId()); }); } } @@ -1546,6 +1550,12 @@ export class AddonModScormProvider { if (success) { // Tracks have been saved, update cached user data. this.updateUserDataAfterSave(scorm.id, attempt, tracks); + + this.eventsProvider.trigger(AddonModScormProvider.DATA_SENT_EVENT, { + scormId: scorm.id, + scoId: scoId, + attempt: attempt + }, this.sitesProvider.getCurrentSiteId()); } return success; diff --git a/src/addon/mod/survey/components/index/addon-mod-survey-index.html b/src/addon/mod/survey/components/index/addon-mod-survey-index.html index 4ad777c3a..01645b24b 100644 --- a/src/addon/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addon/mod/survey/components/index/addon-mod-survey-index.html @@ -3,6 +3,7 @@ + diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index 581064e0e..3a7eb4151 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -188,8 +188,20 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo }); } - return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then(() => { - return this.showLoadingAndRefresh(false); + return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then((online) => { + if (online && this.isPrefetched()) { + // The survey is downloaded, update the data. + return this.surveySync.prefetchAfterUpdate(this.module, this.courseId).then(() => { + // Update the view. + this.showLoadingAndFetch(false, false); + }).catch((error) => { + // Prefetch failed, refresh the data. + return this.showLoadingAndRefresh(false); + }); + } else { + // Not downloaded, refresh the data. + return this.showLoadingAndRefresh(false); + } }).finally(() => { modal.dismiss(); }); diff --git a/src/addon/mod/survey/providers/module-handler.ts b/src/addon/mod/survey/providers/module-handler.ts index 4725b6ea3..ed3368ccb 100644 --- a/src/addon/mod/survey/providers/module-handler.ts +++ b/src/addon/mod/survey/providers/module-handler.ts @@ -64,8 +64,12 @@ export class AddonModSurveyModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_survey-handler', showDownloadButton: true, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModSurveyIndexPage', {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('AddonModSurveyIndexPage', pageParams, options); } }; } diff --git a/src/addon/mod/survey/providers/offline.ts b/src/addon/mod/survey/providers/offline.ts index 6e3ce13a4..5e3185315 100644 --- a/src/addon/mod/survey/providers/offline.ts +++ b/src/addon/mod/survey/providers/offline.ts @@ -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 { CoreTextUtilsProvider } from '@providers/utils/text'; /** @@ -27,42 +27,46 @@ export class AddonModSurveyOfflineProvider { // Variables for database. static SURVEY_TABLE = 'addon_mod_survey_answers'; - protected tablesSchema = [ - { - name: AddonModSurveyOfflineProvider.SURVEY_TABLE, - columns: [ - { - name: 'surveyid', - type: 'INTEGER' - }, - { - name: 'name', - type: 'TEXT' - }, - { - name: 'courseid', - type: 'INTEGER' - }, - { - name: 'userid', - type: 'INTEGER' - }, - { - name: 'answers', - type: 'TEXT' - }, - { - name: 'timecreated', - type: 'INTEGER' - } - ], - primaryKeys: ['surveyid', 'userid'] - } - ]; + protected siteSchema: CoreSiteSchema = { + name: 'AddonModSurveyOfflineProvider', + version: 1, + tables: [ + { + name: AddonModSurveyOfflineProvider.SURVEY_TABLE, + columns: [ + { + name: 'surveyid', + type: 'INTEGER' + }, + { + name: 'name', + type: 'TEXT' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'userid', + type: 'INTEGER' + }, + { + name: 'answers', + type: 'TEXT' + }, + { + name: 'timecreated', + type: 'INTEGER' + } + ], + primaryKeys: ['surveyid', 'userid'] + } + ] + }; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider) { this.logger = logger.getInstance('AddonModSurveyOfflineProvider'); - this.sitesProvider.createTablesFromSchema(this.tablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); } /** diff --git a/src/addon/mod/survey/providers/prefetch-handler.ts b/src/addon/mod/survey/providers/prefetch-handler.ts index b3ac45ea6..e6b785486 100644 --- a/src/addon/mod/survey/providers/prefetch-handler.ts +++ b/src/addon/mod/survey/providers/prefetch-handler.ts @@ -111,7 +111,7 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan * @return {Promise} Promise resolved when done. */ protected prefetchSurvey(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.surveyProvider.getSurvey(courseId, module.id).then((survey) => { + return this.surveyProvider.getSurvey(courseId, module.id, true, siteId).then((survey) => { const promises = [], files = this.getIntroFilesFromInstance(module, survey); @@ -120,7 +120,7 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan // If survey isn't answered, prefetch the questions. if (!survey.surveydone) { - promises.push(this.surveyProvider.getQuestions(survey.id)); + promises.push(this.surveyProvider.getQuestions(survey.id, true, siteId)); } return Promise.all(promises); diff --git a/src/addon/mod/survey/providers/survey.ts b/src/addon/mod/survey/providers/survey.ts index a7ae0337d..ff376e2f8 100644 --- a/src/addon/mod/survey/providers/survey.ts +++ b/src/addon/mod/survey/providers/survey.ts @@ -18,7 +18,9 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModSurveyOfflineProvider } from './offline'; +import { CoreSiteWSPreSets } from '@classes/site'; /** * Service that provides some features for surveys. @@ -32,7 +34,7 @@ export class AddonModSurveyProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private filepoolProvider: CoreFilepoolProvider, private utils: CoreUtilsProvider, - private surveyOffline: AddonModSurveyOfflineProvider) { + private surveyOffline: AddonModSurveyOfflineProvider, private logHelper: CoreCourseLogHelperProvider) { this.logger = logger.getInstance('AddonModSurveyProvider'); } @@ -40,18 +42,24 @@ export class AddonModSurveyProvider { * Get a survey's questions. * * @param {number} surveyId Survey 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} Promise resolved when the questions are retrieved. */ - getQuestions(surveyId: number, siteId?: string): Promise { + getQuestions(surveyId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { surveyid: surveyId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getQuestionsCacheKey(surveyId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_survey_get_questions', params, preSets).then((response) => { if (response.questions) { return response.questions; @@ -86,20 +94,26 @@ export class AddonModSurveyProvider { * Get a survey data. * * @param {number} courseId Course ID. - * @param {string} key Name of the property to check. - * @param {any} value Value to search. + * @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} Promise resolved when the survey is retrieved. */ - protected getSurveyDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getSurveyDataByKey(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getSurveyCacheKey(courseId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_survey_get_surveys_by_courses', params, preSets).then((response) => { if (response && response.surveys) { const currentSurvey = response.surveys.find((survey) => { @@ -119,24 +133,26 @@ export class AddonModSurveyProvider { * Get a survey by course module ID. * * @param {number} courseId Course ID. - * @param {number} cmId Course module ID. + * @param {number} cmId Course 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} Promise resolved when the survey is retrieved. */ - getSurvey(courseId: number, cmId: number, siteId?: string): Promise { - return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, siteId); + getSurvey(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { + return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, ignoreCache, siteId); } /** * Get a survey by ID. * - * @param {number} courseId Course ID. - * @param {number} id Survey ID. + * @param {number} courseId Course ID. + * @param {number} id Survey 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} Promise resolved when the survey is retrieved. */ - getSurveyById(courseId: number, id: number, siteId?: string): Promise { - return this.getSurveyDataByKey(courseId, 'id', id, siteId); + getSurveyById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { + return this.getSurveyDataByKey(courseId, 'id', id, ignoreCache, siteId); } /** @@ -197,14 +213,15 @@ export class AddonModSurveyProvider { * Report the survey as being viewed. * * @param {number} id Module ID. + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ - logView(id: number): Promise { + logView(id: number, siteId?: string): Promise { const params = { surveyid: id }; - return this.sitesProvider.getCurrentSite().write('mod_survey_view_survey', params); + return this.logHelper.log('mod_survey_view_survey', params, AddonModSurveyProvider.COMPONENT, id, siteId); } /** diff --git a/src/addon/mod/survey/providers/sync.ts b/src/addon/mod/survey/providers/sync.ts index 084e05eec..d38b0a898 100644 --- a/src/addon/mod/survey/providers/sync.ts +++ b/src/addon/mod/survey/providers/sync.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -25,25 +24,30 @@ import { AddonModSurveyProvider } from './survey'; import { CoreEventsProvider } from '@providers/events'; import { TranslateService } from '@ngx-translate/core'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { CoreSyncProvider } from '@providers/sync'; +import { AddonModSurveyPrefetchHandler } from './prefetch-handler'; /** * Service to sync surveys. */ @Injectable() -export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { +export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_survey_autom_synced'; protected componentTranslate: string; constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, - courseProvider: CoreCourseProvider, private surveyOffline: AddonModSurveyOfflineProvider, + private courseProvider: CoreCourseProvider, private surveyOffline: AddonModSurveyOfflineProvider, private eventsProvider: CoreEventsProvider, private surveyProvider: AddonModSurveyProvider, - private utils: CoreUtilsProvider, timeUtils: CoreTimeUtilsProvider) { + private utils: CoreUtilsProvider, timeUtils: CoreTimeUtilsProvider, private logHelper: CoreCourseLogHelperProvider, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModSurveyPrefetchHandler) { super('AddonModSurveySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('survey'); } @@ -56,7 +60,7 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { * @return {string} Sync ID. * @protected */ - getSyncId (surveyId: number, userId: number): string { + getSyncId(surveyId: number, userId: number): string { return surveyId + '#' + userId; } @@ -141,10 +145,15 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { answersSent: false }; - // Get answers to be sent. - const syncPromise = this.surveyOffline.getSurveyData(surveyId, siteId, userId).catch(() => { - // No offline data found, return empty object. - return {}; + // Sync offline logs. + const syncPromise = this.logHelper.syncIfNeeded(AddonModSurveyProvider.COMPONENT, surveyId, siteId).catch(() => { + // Ignore errors. + }).then(() => { + // Get answers to be sent. + return this.surveyOffline.getSurveyData(surveyId, siteId, userId).catch(() => { + // No offline data found, return empty object. + return {}; + }); }).then((data) => { if (!data.answers || !data.answers.length) { // Nothing to sync. @@ -186,8 +195,8 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { }).then(() => { if (courseId) { // Data has been sent to server, update survey data. - return this.surveyProvider.invalidateSurveyData(courseId, siteId).then(() => { - return this.surveyProvider.getSurveyById(courseId, surveyId, siteId); + return this.courseProvider.getModuleBasicInfoByInstance(surveyId, 'survey', siteId).then((module) => { + return this.prefetchAfterUpdate(module, courseId, undefined, siteId); }).catch(() => { // Ignore errors. }); diff --git a/src/addon/mod/url/components/index/addon-mod-url-index.html b/src/addon/mod/url/components/index/addon-mod-url-index.html index 422095419..bf2d57265 100644 --- a/src/addon/mod/url/components/index/addon-mod-url-index.html +++ b/src/addon/mod/url/components/index/addon-mod-url-index.html @@ -3,6 +3,7 @@ + diff --git a/src/addon/mod/url/providers/module-handler.ts b/src/addon/mod/url/providers/module-handler.ts index 79c8c54f4..9cf493fd9 100644 --- a/src/addon/mod/url/providers/module-handler.ts +++ b/src/addon/mod/url/providers/module-handler.ts @@ -72,11 +72,15 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_url-handler', showDownloadButton: false, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void { const modal = handler.domUtils.showModalLoading(); - // First of all, check if the URL can be handled by the app. If so, always open it directly. - handler.contentLinksHelper.canHandleLink(module.contents[0].fileurl, courseId).then((canHandle) => { + // First of all, make sure module contents are loaded. + handler.courseProvider.loadModuleContents(module, courseId, undefined, false, false, undefined, handler.modName) + .then(() => { + // Check if the URL can be handled by the app. If so, always open it directly. + return handler.contentLinksHelper.canHandleLink(module.contents[0].fileurl, courseId); + }).then((canHandle) => { if (canHandle) { // URL handled by the app, open it directly. return true; @@ -100,7 +104,11 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler { if (shouldOpen) { handler.openUrl(module, courseId); } else { - navCtrl.push('AddonModUrlIndexPage', {module: module, courseId: courseId}, options); + const pageParams = {module: module, courseId: courseId}; + if (params) { + Object.assign(pageParams, params); + } + navCtrl.push('AddonModUrlIndexPage', pageParams, options); } }).finally(() => { modal.dismiss(); diff --git a/src/addon/mod/url/providers/url.ts b/src/addon/mod/url/providers/url.ts index c1642b6ca..e7e02b85a 100644 --- a/src/addon/mod/url/providers/url.ts +++ b/src/addon/mod/url/providers/url.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreConstants } from '@core/constants'; /** @@ -31,7 +32,8 @@ export class AddonModUrlProvider { protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, - private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider) { + private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider, + private logHelper: CoreCourseLogHelperProvider) { this.logger = logger.getInstance('AddonModUrlProvider'); } @@ -215,13 +217,14 @@ export class AddonModUrlProvider { * Report the url as being viewed. * * @param {number} id Module ID. + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ - logView(id: number): Promise { + logView(id: number, siteId?: string): Promise { const params = { urlid: id }; - return this.sitesProvider.getCurrentSite().write('mod_url_view_url', params); + return this.logHelper.log('mod_url_view_url', params, AddonModUrlProvider.COMPONENT, id, siteId); } } diff --git a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html index 976ae64c9..0a8a6c5b1 100644 --- a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html @@ -13,6 +13,7 @@ + diff --git a/src/addon/mod/wiki/components/index/index.scss b/src/addon/mod/wiki/components/index/index.scss index 9b7a2f796..49867d614 100644 --- a/src/addon/mod/wiki/components/index/index.scss +++ b/src/addon/mod/wiki/components/index/index.scss @@ -7,7 +7,6 @@ $addon-mod-wiki-toc-background-color: $gray-light !default; ion-app.app-root addon-mod-wiki-index { background-color: $white; - .core-tabs-content-container, .addon-mod_wiki-page-content { background-color: $white; } diff --git a/src/addon/mod/wiki/components/index/index.ts b/src/addon/mod/wiki/components/index/index.ts index b154c7f4e..25bc9af76 100644 --- a/src/addon/mod/wiki/components/index/index.ts +++ b/src/addon/mod/wiki/components/index/index.ts @@ -110,7 +110,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp // Ignore errors. }); } else { - this.wikiProvider.logPageView(this.pageId).catch(() => { + this.wikiProvider.logPageView(this.pageId, this.wiki.id).catch(() => { // Ignore errors. }); } @@ -341,7 +341,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp this.currentPage = data.pageId; this.showLoadingAndFetch(true, false).then(() => { - this.wikiProvider.logPageView(this.currentPage).catch(() => { + this.wikiProvider.logPageView(this.currentPage, this.wiki.id).catch(() => { // Ignore errors. }); }); diff --git a/src/addon/mod/wiki/providers/module-handler.ts b/src/addon/mod/wiki/providers/module-handler.ts index 72056f85b..20a94af39 100644 --- a/src/addon/mod/wiki/providers/module-handler.ts +++ b/src/addon/mod/wiki/providers/module-handler.ts @@ -65,8 +65,12 @@ export class AddonModWikiModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_wiki-handler', showDownloadButton: true, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModWikiIndexPage', {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('AddonModWikiIndexPage', pageParams, options); } }; } diff --git a/src/addon/mod/wiki/providers/wiki-offline.ts b/src/addon/mod/wiki/providers/wiki-offline.ts index 6ae9935d4..003fa3b7a 100644 --- a/src/addon/mod/wiki/providers/wiki-offline.ts +++ b/src/addon/mod/wiki/providers/wiki-offline.ts @@ -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'; /** * Service to handle offline wiki. @@ -26,62 +26,66 @@ export class AddonModWikiOfflineProvider { // Variables for database. static NEW_PAGES_TABLE = 'addon_mod_wiki_new_pages_store'; - protected tablesSchema = [ - { - name: AddonModWikiOfflineProvider.NEW_PAGES_TABLE, - columns: [ + protected siteSchema: CoreSiteSchema = { + name: 'AddonModWikiOfflineProvider', + version: 1, + tables: [ { - name: 'wikiid', - type: 'INTEGER' - }, - { - name: 'subwikiid', - type: 'INTEGER' - }, - { - name: 'userid', - type: 'INTEGER' - }, - { - name: 'groupid', - type: 'INTEGER' - }, - { - name: 'title', - type: 'TEXT' - }, - { - name: 'cachedcontent', - type: 'TEXT' - }, - { - name: 'contentformat', - type: 'TEXT' - }, - { - name: 'courseid', - type: 'INTEGER' - }, - { - name: 'timecreated', - type: 'INTEGER' - }, - { - name: 'timemodified', - type: 'INTEGER' - }, - { - name: 'caneditpage', - type: 'INTEGER' - } - ], - primaryKeys: ['wikiid', 'subwikiid', 'userid', 'groupid', 'title'] - } - ]; + name: AddonModWikiOfflineProvider.NEW_PAGES_TABLE, + columns: [ + { + name: 'wikiid', + type: 'INTEGER' + }, + { + name: 'subwikiid', + type: 'INTEGER' + }, + { + name: 'userid', + type: 'INTEGER' + }, + { + name: 'groupid', + type: 'INTEGER' + }, + { + name: 'title', + type: 'TEXT' + }, + { + name: 'cachedcontent', + type: 'TEXT' + }, + { + name: 'contentformat', + type: 'TEXT' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'timecreated', + type: 'INTEGER' + }, + { + name: 'timemodified', + type: 'INTEGER' + }, + { + name: 'caneditpage', + type: 'INTEGER' + } + ], + primaryKeys: ['wikiid', 'subwikiid', 'userid', 'groupid', 'title'] + } + ] + }; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) { this.logger = logger.getInstance('AddonModWikiOfflineProvider'); - this.sitesProvider.createTablesFromSchema(this.tablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); } /** diff --git a/src/addon/mod/wiki/providers/wiki-sync.ts b/src/addon/mod/wiki/providers/wiki-sync.ts index 5a2bdc441..cfd37e1a0 100644 --- a/src/addon/mod/wiki/providers/wiki-sync.ts +++ b/src/addon/mod/wiki/providers/wiki-sync.ts @@ -24,6 +24,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 { CoreSyncBaseProvider } from '@classes/base-sync'; import { AddonModWikiProvider } from './wiki'; import { AddonModWikiOfflineProvider } from './wiki-offline'; @@ -104,7 +105,8 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, private wikiProvider: AddonModWikiProvider, private wikiOfflineProvider: AddonModWikiOfflineProvider, - private utils: CoreUtilsProvider, private groupsProvider: CoreGroupsProvider, timeUtils: CoreTimeUtilsProvider) { + private utils: CoreUtilsProvider, private groupsProvider: CoreGroupsProvider, timeUtils: CoreTimeUtilsProvider, + private logHelper: CoreCourseLogHelperProvider) { super('AddonModWikiSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); @@ -336,8 +338,13 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { syncWiki(wikiId: number, courseId?: number, cmId?: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - // Sync is done at subwiki level, get all the subwikis. - return this.wikiProvider.getSubwikis(wikiId).then((subwikis) => { + // Sync offline logs. + return this.logHelper.syncIfNeeded(AddonModWikiProvider.COMPONENT, wikiId, siteId).catch(() => { + // Ignore errors. + }).then(() => { + // Sync is done at subwiki level, get all the subwikis. + return this.wikiProvider.getSubwikis(wikiId); + }).then((subwikis) => { const promises = [], result: AddonModWikiSyncWikiResult = { warnings: [], diff --git a/src/addon/mod/wiki/providers/wiki.ts b/src/addon/mod/wiki/providers/wiki.ts index ce98a5dff..42dcd8b1c 100644 --- a/src/addon/mod/wiki/providers/wiki.ts +++ b/src/addon/mod/wiki/providers/wiki.ts @@ -20,6 +20,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModWikiOfflineProvider } from './wiki-offline'; import { CoreSiteWSPreSets } from '@classes/site'; @@ -70,7 +71,8 @@ export class AddonModWikiProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private filepoolProvider: CoreFilepoolProvider, private utils: CoreUtilsProvider, private translate: TranslateService, - private wikiOffline: AddonModWikiOfflineProvider, eventsProvider: CoreEventsProvider) { + private wikiOffline: AddonModWikiOfflineProvider, eventsProvider: CoreEventsProvider, + private logHelper: CoreCourseLogHelperProvider) { this.logger = logger.getInstance('AddonModWikiProvider'); // Clear subwiki lists cache on logout. @@ -651,18 +653,17 @@ export class AddonModWikiProvider { /** * Report a wiki page as being viewed. * - * @param {string} id Page ID. + * @param {number} id Page ID. + * @param {number} wikiId Wiki ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ - logPageView(id: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - pageid: id - }; + logPageView(id: number, wikiId: number, siteId?: string): Promise { + const params = { + pageid: id + }; - return site.write('mod_wiki_view_page', params); - }); + return this.logHelper.log('mod_wiki_view_page', params, AddonModWikiProvider.COMPONENT, wikiId, siteId); } /** @@ -673,13 +674,11 @@ export class AddonModWikiProvider { * @return {Promise} Promise resolved when the WS call is successful. */ logView(id: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - wikiid: id - }; + const params = { + wikiid: id + }; - return site.write('mod_wiki_view_wiki', params); - }); + return this.logHelper.log('mod_wiki_view_wiki', params, AddonModWikiProvider.COMPONENT, id, siteId); } /** diff --git a/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts b/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts index 3a845746b..e361727b0 100644 --- a/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts +++ b/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts @@ -120,21 +120,23 @@ export class AddonModWorkshopAssessmentStrategyAccumulativeHandler implements Ad let hasErrors = false; form.fields.forEach((field, idx) => { - const grade = parseInt(currentValues[idx].grade, 10); - if (!isNaN(grade) && grade >= 0) { - data['grade__idx_' + idx] = grade; - } else { - errors['grade_' + idx] = this.translate.instant('addon.mod_workshop_assessment_accumulative.mustchoosegrade'); - hasErrors = true; - } + if (idx < form.dimenssionscount) { + const grade = parseInt(currentValues[idx].grade, 10); + if (!isNaN(grade) && grade >= 0) { + data['grade__idx_' + idx] = grade; + } else { + errors['grade_' + idx] = this.translate.instant('addon.mod_workshop_assessment_accumulative.mustchoosegrade'); + hasErrors = true; + } - if (currentValues[idx].peercomment) { - data['peercomment__idx_' + idx] = currentValues[idx].peercomment; - } + if (currentValues[idx].peercomment) { + data['peercomment__idx_' + idx] = currentValues[idx].peercomment; + } - data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; - data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); - data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0; + data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; + data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0; + } }); if (hasErrors) { diff --git a/src/addon/mod/workshop/assessment/comments/providers/handler.ts b/src/addon/mod/workshop/assessment/comments/providers/handler.ts index 339c16a88..132e0f38b 100644 --- a/src/addon/mod/workshop/assessment/comments/providers/handler.ts +++ b/src/addon/mod/workshop/assessment/comments/providers/handler.ts @@ -102,15 +102,17 @@ export class AddonModWorkshopAssessmentStrategyCommentsHandler implements AddonW let hasErrors = false; form.fields.forEach((field, idx) => { - if (currentValues[idx].peercomment) { - data['peercomment__idx_' + idx] = currentValues[idx].peercomment; - } else { - errors['peercomment_' + idx] = this.translate.instant('core.err_required'); - hasErrors = true; - } + if (idx < form.dimenssionscount) { + if (currentValues[idx].peercomment) { + data['peercomment__idx_' + idx] = currentValues[idx].peercomment; + } else { + errors['peercomment_' + idx] = this.translate.instant('core.err_required'); + hasErrors = true; + } - data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; - data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; + data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + } }); if (hasErrors) { diff --git a/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts b/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts index 4eaaf2012..5e77dc7e0 100644 --- a/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts +++ b/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts @@ -106,21 +106,23 @@ export class AddonModWorkshopAssessmentStrategyNumErrorsHandler implements Addon let hasErrors = false; form.fields.forEach((field, idx) => { - const grade = parseInt(currentValues[idx].grade); - if (!isNaN(grade) && grade >= 0) { - data['grade__idx_' + idx] = grade; - } else { - errors['grade_' + idx] = this.translate.instant('core.required'); - hasErrors = true; - } + if (idx < form.dimenssionscount) { + const grade = parseInt(currentValues[idx].grade); + if (!isNaN(grade) && (grade == 1 || grade == -1)) { + data['grade__idx_' + idx] = grade; + } else { + errors['grade_' + idx] = this.translate.instant('core.required'); + hasErrors = true; + } - if (currentValues[idx].peercomment) { - data['peercomment__idx_' + idx] = currentValues[idx].peercomment; - } + if (currentValues[idx].peercomment) { + data['peercomment__idx_' + idx] = currentValues[idx].peercomment; + } - data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; - data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); - data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0; + data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; + data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0; + } }); if (hasErrors) { diff --git a/src/addon/mod/workshop/assessment/rubric/providers/handler.ts b/src/addon/mod/workshop/assessment/rubric/providers/handler.ts index bc5dbdb1f..d9dc5a38b 100644 --- a/src/addon/mod/workshop/assessment/rubric/providers/handler.ts +++ b/src/addon/mod/workshop/assessment/rubric/providers/handler.ts @@ -102,16 +102,18 @@ export class AddonModWorkshopAssessmentStrategyRubricHandler implements AddonWor let hasErrors = false; form.fields.forEach((field, idx) => { - const id = parseInt(currentValues[idx].chosenlevelid, 10); - if (!isNaN(id) && id >= 0) { - data['chosenlevelid__idx_' + idx] = id; - } else { - errors['chosenlevelid_' + idx] = this.translate.instant('addon.mod_workshop_assessment_rubric.mustchooseone'); - hasErrors = true; - } + if (idx < form.dimenssionscount) { + const id = parseInt(currentValues[idx].chosenlevelid, 10); + if (!isNaN(id) && id >= 0) { + data['chosenlevelid__idx_' + idx] = id; + } else { + errors['chosenlevelid_' + idx] = this.translate.instant('addon.mod_workshop_assessment_rubric.mustchooseone'); + hasErrors = true; + } - data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; - data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; + data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + } }); if (hasErrors) { diff --git a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html index bd3b2b12f..f778b5f19 100644 --- a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html +++ b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html @@ -3,6 +3,7 @@ + diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index 419ef4d4a..1515e91da 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -32,6 +32,7 @@ import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivityComponent { @Input() group = 0; + component = AddonModWorkshopProvider.COMPONENT; moduleName = 'workshop'; workshop: any; page = 0; @@ -64,10 +65,11 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity protected obsAssessmentSaved: any; protected appResumeSubscription: any; protected syncObserver: any; + protected syncEventName = AddonModWorkshopSyncProvider.AUTO_SYNCED; constructor(injector: Injector, private workshopProvider: AddonModWorkshopProvider, @Optional() content: Content, private workshopOffline: AddonModWorkshopOfflineProvider, private groupsProvider: CoreGroupsProvider, - private navCtrl: NavController, private modalCtrl: ModalController, private utils: CoreUtilsProvider, + protected navCtrl: NavController, private modalCtrl: ModalController, private utils: CoreUtilsProvider, platform: Platform, private workshopHelper: AddonModWorkshopHelperProvider, private workshopSync: AddonModWorkshopSyncProvider) { super(injector, content); diff --git a/src/addon/mod/workshop/components/submission/addon-mod-workshop-submission.html b/src/addon/mod/workshop/components/submission/addon-mod-workshop-submission.html index 2637cc500..180f298c8 100644 --- a/src/addon/mod/workshop/components/submission/addon-mod-workshop-submission.html +++ b/src/addon/mod/workshop/components/submission/addon-mod-workshop-submission.html @@ -1,7 +1,7 @@
- +

{{submission.title}}

{{profile.fullname}}

diff --git a/src/addon/mod/workshop/pages/assessment/assessment.html b/src/addon/mod/workshop/pages/assessment/assessment.html index 4834ab4ca..ecc8b3fd3 100644 --- a/src/addon/mod/workshop/pages/assessment/assessment.html +++ b/src/addon/mod/workshop/pages/assessment/assessment.html @@ -36,7 +36,7 @@ - + diff --git a/src/addon/mod/workshop/pages/assessment/assessment.ts b/src/addon/mod/workshop/pages/assessment/assessment.ts index 22c1b2bde..09b6a1e3f 100644 --- a/src/addon/mod/workshop/pages/assessment/assessment.ts +++ b/src/addon/mod/workshop/pages/assessment/assessment.ts @@ -164,8 +164,8 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { if (this.evaluating || this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { // Get all info of the assessment. - return this.workshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, this.profile.id) - .then((assessment) => { + return this.workshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, + this.profile && this.profile.id).then((assessment) => { let defaultGrade, promise; this.assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); diff --git a/src/addon/mod/workshop/pages/phase/phase.scss b/src/addon/mod/workshop/pages/phase/phase.scss index 6b909c4ea..f30416066 100644 --- a/src/addon/mod/workshop/pages/phase/phase.scss +++ b/src/addon/mod/workshop/pages/phase/phase.scss @@ -1,15 +1,6 @@ ion-app.app-root page-addon-mod-workshop-phase-info { .core-workshop-phase-selected { background-color: $white; - @include border-start(5px, solid, $core-splitview-selected); - &.item-md { - @include padding(null, null, null, $item-md-padding-start - 5px); - } - &.item-ios { - @include padding(null, null, null, $item-ios-padding-start - 5px); - } - &.item-wp { - @include padding(null, null, null, $item-wp-padding-start - 5px); - } + @include core-selected-item($core-splitview-selected); } } \ No newline at end of file diff --git a/src/addon/mod/workshop/pages/submission/submission.ts b/src/addon/mod/workshop/pages/submission/submission.ts index 218bfe2c7..e4cb2cd5f 100644 --- a/src/addon/mod/workshop/pages/submission/submission.ts +++ b/src/addon/mod/workshop/pages/submission/submission.ts @@ -130,7 +130,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { */ ngOnInit(): void { this.fetchSubmissionData().then(() => { - this.workshopProvider.logViewSubmission(this.submissionId).then(() => { + this.workshopProvider.logViewSubmission(this.submissionId, this.workshopId).then(() => { this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); }).catch(() => { // Ignore errors. @@ -202,7 +202,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { this.workshop.phase < AddonModWorkshopProvider.PHASE_CLOSED && this.access.canoverridegrades; this.ownAssessment = false; - if (this.access.canviewallassessments) { + if (this.access.canviewallassessments || this.currentUserId == this.userId) { // Get new data, different that came from stateParams. promises.push(this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId) .then((subAssessments) => { @@ -291,7 +291,8 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { this.feedbackForm.controls['text'].setValue(this.evaluate.text); }); })); - } else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && submissionData.gradeoverby) { + } else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && submissionData.gradeoverby && + this.evaluate && this.evaluate.text) { promises.push(this.userProvider.getProfile(submissionData.gradeoverby, this.courseId, true).then((profile) => { this.evaluateByProfile = profile; })); diff --git a/src/addon/mod/workshop/providers/module-handler.ts b/src/addon/mod/workshop/providers/module-handler.ts index 11914daa9..addfe132d 100644 --- a/src/addon/mod/workshop/providers/module-handler.ts +++ b/src/addon/mod/workshop/providers/module-handler.ts @@ -64,8 +64,12 @@ export class AddonModWorkshopModuleHandler implements CoreCourseModuleHandler { title: module.name, class: 'addon-mod_workshop-handler', showDownloadButton: true, - action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModWorkshopIndexPage', {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('AddonModWorkshopIndexPage', pageParams, options); } }; } diff --git a/src/addon/mod/workshop/providers/offline.ts b/src/addon/mod/workshop/providers/offline.ts index 63ed454a8..dcd7f1adc 100644 --- a/src/addon/mod/workshop/providers/offline.ts +++ b/src/addon/mod/workshop/providers/offline.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreFileProvider } from '@providers/file'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -30,146 +30,150 @@ export class AddonModWorkshopOfflineProvider { static EVALUATE_SUBMISSIONS_TABLE = 'addon_mod_workshop_evaluate_submissions'; static EVALUATE_ASSESSMENTS_TABLE = 'addon_mod_workshop_evaluate_assessments'; - protected tablesSchema = [ - { - name: AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, - columns: [ - { - name: 'workshopid', - type: 'INTEGER', - }, - { - name: 'submissionid', - type: 'INTEGER', - }, - { - name: 'action', - type: 'TEXT', - }, - { - name: 'courseid', - type: 'INTEGER', - }, - { - name: 'title', - type: 'TEXT', - }, - { - name: 'content', - type: 'TEXT', - }, - { - name: 'attachmentsid', - type: 'TEXT', - }, - { - name: 'timemodified', - type: 'INTEGER', - } - ], - primaryKeys: ['workshopid', 'submissionid', 'action'] - }, - { - name: AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, - columns: [ - { - name: 'workshopid', - type: 'INTEGER', - }, - { - name: 'assessmentid', - type: 'INTEGER', - }, - { - name: 'courseid', - type: 'INTEGER', - }, - { - name: 'inputdata', - type: 'TEXT', - }, - { - name: 'timemodified', - type: 'INTEGER', - }, - ], - primaryKeys: ['workshopid', 'assessmentid'] - }, - { - name: AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, - columns: [ - { - name: 'workshopid', - type: 'INTEGER', - }, - { - name: 'submissionid', - type: 'INTEGER', - }, - { - name: 'courseid', - type: 'INTEGER', - }, - { - name: 'timemodified', - type: 'INTEGER', - }, - { - name: 'feedbacktext', - type: 'TEXT', - }, - { - name: 'published', - type: 'INTEGER', - }, - { - name: 'gradeover', - type: 'TEXT', - }, - ], - primaryKeys: ['workshopid', 'submissionid'] - }, - { - name: AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, - columns: [ - { - name: 'workshopid', - type: 'INTEGER', - }, - { - name: 'assessmentid', - type: 'INTEGER', - }, - { - name: 'courseid', - type: 'INTEGER', - }, - { - name: 'timemodified', - type: 'INTEGER', - }, - { - name: 'feedbacktext', - type: 'TEXT', - }, - { - name: 'weight', - type: 'INTEGER', - }, - { - name: 'gradinggradeover', - type: 'TEXT', - }, - ], - primaryKeys: ['workshopid', 'assessmentid'] - } - ]; + protected siteSchema: CoreSiteSchema = { + name: 'AddonModWorkshopOfflineProvider', + version: 1, + tables: [ + { + name: AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, + columns: [ + { + name: 'workshopid', + type: 'INTEGER', + }, + { + name: 'submissionid', + type: 'INTEGER', + }, + { + name: 'action', + type: 'TEXT', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'title', + type: 'TEXT', + }, + { + name: 'content', + type: 'TEXT', + }, + { + name: 'attachmentsid', + type: 'TEXT', + }, + { + name: 'timemodified', + type: 'INTEGER', + } + ], + primaryKeys: ['workshopid', 'submissionid', 'action'] + }, + { + name: AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, + columns: [ + { + name: 'workshopid', + type: 'INTEGER', + }, + { + name: 'assessmentid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'inputdata', + type: 'TEXT', + }, + { + name: 'timemodified', + type: 'INTEGER', + }, + ], + primaryKeys: ['workshopid', 'assessmentid'] + }, + { + name: AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, + columns: [ + { + name: 'workshopid', + type: 'INTEGER', + }, + { + name: 'submissionid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'timemodified', + type: 'INTEGER', + }, + { + name: 'feedbacktext', + type: 'TEXT', + }, + { + name: 'published', + type: 'INTEGER', + }, + { + name: 'gradeover', + type: 'TEXT', + }, + ], + primaryKeys: ['workshopid', 'submissionid'] + }, + { + name: AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, + columns: [ + { + name: 'workshopid', + type: 'INTEGER', + }, + { + name: 'assessmentid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'timemodified', + type: 'INTEGER', + }, + { + name: 'feedbacktext', + type: 'TEXT', + }, + { + name: 'weight', + type: 'INTEGER', + }, + { + name: 'gradinggradeover', + type: 'TEXT', + }, + ], + primaryKeys: ['workshopid', 'assessmentid'] + } + ] + }; constructor(private fileProvider: CoreFileProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { - this.sitesProvider.createTablesFromSchema(this.tablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); } /** diff --git a/src/addon/mod/workshop/providers/prefetch-handler.ts b/src/addon/mod/workshop/providers/prefetch-handler.ts index d7e213044..ccdb6c4d2 100644 --- a/src/addon/mod/workshop/providers/prefetch-handler.ts +++ b/src/addon/mod/workshop/providers/prefetch-handler.ts @@ -332,17 +332,14 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH }); } - if (assessments.length > 0) { - reportPromise = reportPromise.finally(() => { - const promises3 = []; - assessments.forEach((assessment, id) => { - promises3.push(this.workshopProvider.getAssessmentForm(workshop.id, id, undefined, undefined, - undefined, siteId)); - }); - - return Promise.all(promises3); - }); - } + reportPromise = reportPromise.finally(() => { + if (assessments.length > 0) { + return Promise.all(assessments.map((assessment, id) => { + return this.workshopProvider.getAssessmentForm(workshop.id, id, undefined, undefined, undefined, + siteId); + })); + } + }); promises2.push(reportPromise); if (workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { diff --git a/src/addon/mod/workshop/providers/sync.ts b/src/addon/mod/workshop/providers/sync.ts index f6693960f..74f7619cf 100644 --- a/src/addon/mod/workshop/providers/sync.ts +++ b/src/addon/mod/workshop/providers/sync.ts @@ -24,6 +24,7 @@ import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModWorkshopProvider } from './workshop'; import { AddonModWorkshopHelperProvider } from './helper'; import { AddonModWorkshopOfflineProvider } from './offline'; @@ -51,7 +52,8 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { private utils: CoreUtilsProvider, private workshopProvider: AddonModWorkshopProvider, private workshopHelper: AddonModWorkshopHelperProvider, - private workshopOffline: AddonModWorkshopOfflineProvider) { + private workshopOffline: AddonModWorkshopOfflineProvider, + private logHelper: CoreCourseLogHelperProvider) { super('AddonModWorkshopSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); @@ -172,6 +174,9 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { return []; })); + // Sync offline logs. + syncPromises.push(this.logHelper.syncIfNeeded(AddonModWorkshopProvider.COMPONENT, workshopId, siteId)); + const result = { warnings: [], updated: false @@ -353,7 +358,15 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { result.updated = true; return this.workshopOffline.deleteSubmissionAction(action.workshopid, action.submissionid, action.action, - siteId); + siteId).then(() => { + // Delete stored files. + if (action.action == 'add' || action.action == 'update') { + const editing = action.action == 'update'; + + return this.workshopHelper.deleteSubmissionStoredFiles(action.workshopid, + action.submissionid, editing, siteId); + } + }); }); }); }); @@ -433,7 +446,9 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { // Delete the offline data. result.updated = true; - return this.workshopOffline.deleteAssessment(workshop.id, assessmentId, siteId); + return this.workshopOffline.deleteAssessment(workshop.id, assessmentId, siteId).then(() => { + this.workshopHelper.deleteAssessmentStoredFiles(workshop.id, assessmentId, siteId); + }); }); }).then(() => { if (discardError) { diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index 8c22710ef..5d0e3165e 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -17,6 +17,7 @@ import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModWorkshopOfflineProvider } from './offline'; /** @@ -49,7 +50,8 @@ export class AddonModWorkshopProvider { private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, - private workshopOffline: AddonModWorkshopOfflineProvider) {} + private workshopOffline: AddonModWorkshopOfflineProvider, + private logHelper: CoreCourseLogHelperProvider) {} /** * Get cache key for workshop data WS calls. @@ -1359,34 +1361,31 @@ export class AddonModWorkshopProvider { /** * Report the workshop as being viewed. * - * @param {string} id Workshop ID. + * @param {number} id Workshop ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ logView(id: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - workshopid: id - }; + const params = { + workshopid: id + }; - return site.write('mod_workshop_view_workshop', params); - }); + return this.logHelper.log('mod_workshop_view_workshop', params, AddonModWorkshopProvider.COMPONENT, id, siteId); } /** * Report the workshop submission as being viewed. * - * @param {string} id Submission ID. + * @param {number} id Submission ID. + * @param {number} workshopId Workshop ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ - logViewSubmission(id: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - submissionid: id - }; + logViewSubmission(id: number, workshopId: number, siteId?: string): Promise { + const params = { + submissionid: id + }; - return site.write('mod_workshop_view_submission', params); - }); + return this.logHelper.log('mod_workshop_view_submission', params, AddonModWorkshopProvider.COMPONENT, workshopId, siteId); } } diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html index 0285ddd90..2c069ca78 100644 --- a/src/addon/notes/components/list/addon-notes-list.html +++ b/src/addon/notes/components/list/addon-notes-list.html @@ -10,6 +10,10 @@ + + +

{{user.fullname}}

+
@@ -29,8 +33,8 @@ - -

{{note.userfullname}}

+ +

{{note.userfullname}}

{{note.lastmodified | coreDateDayOrTime}}

{{ 'core.notsent' | translate }}

@@ -39,5 +43,11 @@
+ + + + diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts index b51247b3d..0150edbfd 100644 --- a/src/addon/notes/components/list/list.ts +++ b/src/addon/notes/components/list/list.ts @@ -13,11 +13,12 @@ // limitations under the License. import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { Content } from 'ionic-angular'; +import { Content, ModalController } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUserProvider } from '@core/user/providers/user'; import { AddonNotesProvider } from '../../providers/notes'; import { AddonNotesSyncProvider } from '../../providers/notes-sync'; @@ -30,6 +31,7 @@ import { AddonNotesSyncProvider } from '../../providers/notes-sync'; }) export class AddonNotesListComponent implements OnInit, OnDestroy { @Input() courseId: number; + @Input() userId?: number; @ViewChild(Content) content: Content; @@ -41,10 +43,12 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { notes: any[]; hasOffline = false; notesLoaded = false; + user: any; constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, - sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, - private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider) { + sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, private modalCtrl: ModalController, + private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider, + private userProvider: CoreUserProvider) { // Refresh data if notes are synchronized automatically. this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => { if (data.courseId == this.courseId) { @@ -67,7 +71,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { */ ngOnInit(): void { this.fetchNotes(true).then(() => { - this.notesProvider.logView(this.courseId).catch(() => { + this.notesProvider.logView(this.courseId, this.userId).catch(() => { // Ignore errors. }); }); @@ -86,14 +90,23 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { return promise.catch(() => { // Ignore errors. }).then(() => { - return this.notesProvider.getNotes(this.courseId).then((notes) => { + return this.notesProvider.getNotes(this.courseId, this.userId).then((notes) => { notes = notes[this.type + 'notes'] || []; this.hasOffline = notes.some((note) => note.offline); - return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + if (this.userId) { this.notes = notes; - }); + + // Get the user profile to retrieve the user image. + return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { + this.user = user; + }); + } else { + return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + this.notes = notes; + }); + } }); }).catch((message) => { this.domUtils.showErrorModal(message); @@ -113,7 +126,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { refreshNotes(showErrors: boolean, refresher?: any): void { this.refreshIcon = 'spinner'; this.syncIcon = 'spinner'; - this.notesProvider.invalidateNotes(this.courseId).finally(() => { + this.notesProvider.invalidateNotes(this.courseId, this.userId).finally(() => { this.fetchNotes(true, showErrors).finally(() => { if (refresher) { refresher.complete(); @@ -130,12 +143,36 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { this.refreshIcon = 'spinner'; this.syncIcon = 'spinner'; this.fetchNotes(true).then(() => { - this.notesProvider.logView(this.courseId).catch(() => { + this.notesProvider.logView(this.courseId, this.userId).catch(() => { // Ignore errors. }); }); } + /** + * Add a new Note to user and course. + * @param {Event} e Event. + */ + addNote(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + const modal = this.modalCtrl.create('AddonNotesAddPage', { userId: this.userId, courseId: this.courseId, type: this.type }); + modal.onDidDismiss((data) => { + if (data && data.sent && data.type) { + if (data.type != this.type) { + this.type = data.type; + this.notesLoaded = false; + } + + this.refreshNotes(true); + } else if (data && data.type && data.type != this.type) { + this.type = data.type; + this.typeChanged(); + } + }); + modal.present(); + } + /** * Tries to synchronize course notes. * diff --git a/src/addon/notes/notes.module.ts b/src/addon/notes/notes.module.ts index 425766440..002f60177 100644 --- a/src/addon/notes/notes.module.ts +++ b/src/addon/notes/notes.module.ts @@ -44,8 +44,7 @@ export const ADDON_NOTES_PROVIDERS: any[] = [ AddonNotesSyncProvider, AddonNotesCourseOptionHandler, AddonNotesSyncCronHandler, - AddonNotesUserHandler - ] + AddonNotesUserHandler ] }) export class AddonNotesModule { constructor(courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonNotesCourseOptionHandler, diff --git a/src/addon/notes/pages/add/add.html b/src/addon/notes/pages/add/add.html index aaf3bfab8..b6df1d660 100644 --- a/src/addon/notes/pages/add/add.html +++ b/src/addon/notes/pages/add/add.html @@ -8,11 +8,11 @@ - + {{ 'addon.notes.publishstate' | translate }} - + {{ 'addon.notes.personalnotes' | translate }} {{ 'addon.notes.coursenotes' | translate }} {{ 'addon.notes.sitenotes' | translate }} @@ -21,8 +21,10 @@ - +
+ +
diff --git a/src/addon/notes/pages/add/add.ts b/src/addon/notes/pages/add/add.ts index 92cc7e219..98c770c8f 100644 --- a/src/addon/notes/pages/add/add.ts +++ b/src/addon/notes/pages/add/add.ts @@ -29,7 +29,7 @@ import { AddonNotesProvider } from '../../providers/notes'; export class AddonNotesAddPage { userId: number; courseId: number; - publishState = 'personal'; + type = 'personal'; text = ''; processing = false; @@ -37,6 +37,7 @@ export class AddonNotesAddPage { private domUtils: CoreDomUtilsProvider, private notesProvider: AddonNotesProvider) { this.userId = params.get('userId'); this.courseId = params.get('courseId'); + this.type = params.get('type') || 'personal'; } /** @@ -52,10 +53,9 @@ export class AddonNotesAddPage { const loadingModal = this.domUtils.showModalLoading('core.sending', true); // Freeze the add note button. this.processing = true; - this.notesProvider.addNote(this.userId, this.courseId, this.publishState, this.text).then((sent) => { - this.viewCtrl.dismiss().finally(() => { - const message = sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline'; - this.domUtils.showAlertTranslated('core.success', message); + this.notesProvider.addNote(this.userId, this.courseId, this.type, this.text).then((sent) => { + this.viewCtrl.dismiss({type: this.type, sent: true}).finally(() => { + this.domUtils.showToast(sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline', true, 3000); }); }).catch((error) => { this.domUtils.showErrorModal(error); @@ -69,6 +69,6 @@ export class AddonNotesAddPage { * Close modal. */ closeModal(): void { - this.viewCtrl.dismiss(); + this.viewCtrl.dismiss({type: this.type}); } } diff --git a/src/addon/notes/pages/list/list.html b/src/addon/notes/pages/list/list.html new file mode 100644 index 000000000..f88fe2201 --- /dev/null +++ b/src/addon/notes/pages/list/list.html @@ -0,0 +1,7 @@ + + + {{ 'addon.notes.notes' | translate }} + + + + diff --git a/src/addon/notes/pages/list/list.module.ts b/src/addon/notes/pages/list/list.module.ts new file mode 100644 index 000000000..c983a9395 --- /dev/null +++ b/src/addon/notes/pages/list/list.module.ts @@ -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 { AddonNotesListPage } from './list'; +import { AddonNotesComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + AddonNotesListPage + ], + imports: [ + CoreDirectivesModule, + AddonNotesComponentsModule, + IonicPageModule.forChild(AddonNotesListPage), + TranslateModule.forChild() + ] +}) +export class AddonNotesListPageModule {} diff --git a/src/addon/notes/pages/list/list.ts b/src/addon/notes/pages/list/list.ts new file mode 100644 index 000000000..499ca2304 --- /dev/null +++ b/src/addon/notes/pages/list/list.ts @@ -0,0 +1,34 @@ +// (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 a list of notes. + */ +@IonicPage({ segment: 'addon-notes-list-page' }) +@Component({ + selector: 'page-addon-notes-list-page', + templateUrl: 'list.html', +}) +export class AddonNotesListPage { + userId: number; + courseId: number; + + constructor(params: NavParams) { + this.userId = params.get('userId'); + this.courseId = params.get('courseId'); + } +} diff --git a/src/addon/notes/providers/course-option-handler.ts b/src/addon/notes/providers/course-option-handler.ts index a1431a8c3..f00761ae8 100644 --- a/src/addon/notes/providers/course-option-handler.ts +++ b/src/addon/notes/providers/course-option-handler.ts @@ -79,6 +79,6 @@ export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler { * @return {Promise} Promise resolved when done. */ prefetch(course: any): Promise { - return this.notesProvider.getNotes(course.id, true); + return this.notesProvider.getNotes(course.id, undefined, true); } } diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts index b75a69067..486fa0111 100644 --- a/src/addon/notes/providers/notes-offline.ts +++ b/src/addon/notes/providers/notes-offline.ts @@ -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 { CoreTimeUtilsProvider } from '@providers/utils/time'; /** @@ -26,46 +26,50 @@ export class AddonNotesOfflineProvider { // Variables for database. static NOTES_TABLE = 'addon_notes_offline_notes'; - protected tablesSchema = [ - { - name: AddonNotesOfflineProvider.NOTES_TABLE, - columns: [ - { - name: 'userid', - type: 'INTEGER' - }, - { - name: 'courseid', - type: 'INTEGER' - }, - { - name: 'publishstate', - type: 'TEXT', - }, - { - name: 'content', - type: 'TEXT' - }, - { - name: 'format', - type: 'INTEGER' - }, - { - name: 'created', - type: 'INTEGER' - }, - { - name: 'lastmodified', - type: 'INTEGER' - } - ], - primaryKeys: ['userid', 'content', 'created'] - } - ]; + protected siteSchema: CoreSiteSchema = { + name: 'AddonNotesOfflineProvider', + version: 1, + tables: [ + { + name: AddonNotesOfflineProvider.NOTES_TABLE, + columns: [ + { + name: 'userid', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'publishstate', + type: 'TEXT', + }, + { + name: 'content', + type: 'TEXT' + }, + { + name: 'format', + type: 'INTEGER' + }, + { + name: 'created', + type: 'INTEGER' + }, + { + name: 'lastmodified', + type: 'INTEGER' + } + ], + primaryKeys: ['userid', 'content', 'created'] + } + ] + }; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) { this.logger = logger.getInstance('AddonNotesOfflineProvider'); - this.sitesProvider.createTablesFromSchema(this.tablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); } /** @@ -118,6 +122,24 @@ export class AddonNotesOfflineProvider { }); } + /** + * Get offline notes for a certain course and user. + * + * @param {number} courseId Course ID. + * @param {number} [userId] User ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getNotesForCourseAndUser(courseId: number, userId?: number, siteId?: string): Promise { + if (!userId) { + return this.getNotesForCourse(courseId, siteId); + } + + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_TABLE, {userid: userId, courseid: courseId}); + }); + } + /** * Get offline notes for a certain course. * diff --git a/src/addon/notes/providers/notes-sync.ts b/src/addon/notes/providers/notes-sync.ts index e02baf438..376e5b673 100644 --- a/src/addon/notes/providers/notes-sync.ts +++ b/src/addon/notes/providers/notes-sync.ts @@ -154,8 +154,8 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { }); // Fetch the notes from server to be sure they're up to date. - return this.notesProvider.invalidateNotes(courseId, siteId).then(() => { - return this.notesProvider.getNotes(courseId, false, true, siteId); + return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => { + return this.notesProvider.getNotes(courseId, undefined, false, true, siteId); }).catch(() => { // Ignore errors. }); diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts index 9e065ca01..8226e0139 100644 --- a/src/addon/notes/providers/notes.ts +++ b/src/addon/notes/providers/notes.ts @@ -104,7 +104,7 @@ export class AddonNotesProvider { } // A note was added, invalidate the course notes. - return this.invalidateNotes(courseId, siteId).catch(() => { + return this.invalidateNotes(courseId, undefined, siteId).catch(() => { // Ignore errors. }); }); @@ -184,37 +184,54 @@ export class AddonNotesProvider { * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. */ isPluginViewNotesEnabledForCourse(courseId: number, siteId?: string): Promise { - return this.utils.promiseWorks(this.getNotes(courseId, false, true, siteId)); + return this.utils.promiseWorks(this.getNotes(courseId, undefined, false, true, siteId)); + } + + /** + * Get prefix cache key for course notes. + * + * @param {number} courseId ID of the course to get the notes from. + * @return {string} Cache key. + */ + getNotesPrefixCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'notes:' + courseId + ':'; } /** * Get the cache key for the get notes call. * * @param {number} courseId ID of the course to get the notes from. + * @param {number} [userId] ID of the user to get the notes from if requested. * @return {string} Cache key. */ - getNotesCacheKey(courseId: number): string { - return this.ROOT_CACHE_KEY + 'notes:' + courseId; + getNotesCacheKey(courseId: number, userId?: number): string { + return this.getNotesPrefixCacheKey(courseId) + (userId ? userId : ''); } /** * Get users notes for a certain site, course and personal notes. * * @param {number} courseId ID of the course to get the notes from. + * @param {number} [userId] ID of the user to get the notes from if requested. * @param {boolean} [ignoreCache] True when we should not get the value from the cache. * @param {boolean} [onlyOnline] True to return only online notes, false to return both online and offline. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise to be resolved when the notes are retrieved. */ - getNotes(courseId: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string): Promise { + getNotes(courseId: number, userId?: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string): Promise { this.logger.debug('Get notes for course ' + courseId); return this.sitesProvider.getSite(siteId).then((site) => { const data = { courseid: courseId }; + + if (userId) { + data['userid'] = userId; + } + const preSets: CoreSiteWSPreSets = { - cacheKey: this.getNotesCacheKey(courseId) + cacheKey: this.getNotesCacheKey(courseId, userId) }; if (ignoreCache) { @@ -228,7 +245,7 @@ export class AddonNotesProvider { } // Get offline notes and add them to the list. - return this.notesOffline.getNotesForCourse(courseId, siteId).then((offlineNotes) => { + return this.notesOffline.getNotesForCourseAndUser(courseId, userId, siteId).then((offlineNotes) => { offlineNotes.forEach((note) => { const fieldName = note.publishstate + 'notes'; if (!notes[fieldName]) { @@ -272,12 +289,17 @@ export class AddonNotesProvider { * Invalidate get notes WS call. * * @param {number} courseId Course ID. + * @param {number} [userId] User ID if needed. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when data is invalidated. */ - invalidateNotes(courseId: number, siteId?: string): Promise { + invalidateNotes(courseId: number, userId?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId)); + if (userId) { + return site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId, userId)); + } + + return site.invalidateWsCacheForKeyStartingWith(this.getNotesPrefixCacheKey(courseId)); }); } @@ -285,14 +307,15 @@ export class AddonNotesProvider { * Report notes as being viewed. * * @param {number} courseId ID of the course. + * @param {number} [userId] User ID if needed. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ - logView(courseId: number, siteId?: string): Promise { + logView(courseId: number, userId?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseid: courseId, - userid: 0 + userid: userId || 0 }; return site.write('core_notes_view_notes', params); diff --git a/src/addon/notes/providers/user-handler.ts b/src/addon/notes/providers/user-handler.ts index ffa1c367c..a348ffb13 100644 --- a/src/addon/notes/providers/user-handler.ts +++ b/src/addon/notes/providers/user-handler.ts @@ -13,10 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { ModalController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreSitesProvider } from '@providers/sites'; import { AddonNotesProvider } from './notes'; @@ -25,30 +25,30 @@ import { AddonNotesProvider } from './notes'; */ @Injectable() export class AddonNotesUserHandler implements CoreUserProfileHandler { - name = 'AddonNotes:addNote'; - priority = 200; - type = CoreUserDelegate.TYPE_COMMUNICATION; - addNoteEnabledCache = {}; + name = 'AddonNotes:notes'; + priority = 100; + type = CoreUserDelegate.TYPE_NEW_PAGE; + noteEnabledCache = {}; - constructor(private modalCtrl: ModalController, private sitesProvider: CoreSitesProvider, + constructor(private linkHelper: CoreContentLinksHelperProvider, private sitesProvider: CoreSitesProvider, private notesProvider: AddonNotesProvider, eventsProvider: CoreEventsProvider) { - eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearAddNoteCache.bind(this)); + eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearNoteCache.bind(this)); eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { - this.clearAddNoteCache(data.courseId); + this.clearNoteCache(data.courseId); }); } /** - * Clear add note cache. + * Clear note cache. * If a courseId is specified, it will only delete the entry for that course. * * @param {number} [courseId] Course ID. */ - private clearAddNoteCache(courseId?: number): void { + private clearNoteCache(courseId?: number): void { if (courseId) { - delete this.addNoteEnabledCache[courseId]; + delete this.noteEnabledCache[courseId]; } else { - this.addNoteEnabledCache = {}; + this.noteEnabledCache = {}; } } @@ -75,12 +75,12 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler { return Promise.resolve(false); } - if (typeof this.addNoteEnabledCache[courseId] != 'undefined') { - return this.addNoteEnabledCache[courseId]; + if (typeof this.noteEnabledCache[courseId] != 'undefined') { + return this.noteEnabledCache[courseId]; } - return this.notesProvider.isPluginAddNoteEnabledForCourse(courseId).then((enabled) => { - this.addNoteEnabledCache[courseId] = enabled; + return this.notesProvider.isPluginViewNotesEnabledForCourse(courseId).then((enabled) => { + this.noteEnabledCache[courseId] = enabled; return enabled; }); @@ -94,13 +94,13 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler { getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { return { icon: 'list', - title: 'addon.notes.addnewnote', + title: 'addon.notes.notes', class: 'addon-notes-handler', action: (event, navCtrl, user, courseId): void => { event.preventDefault(); event.stopPropagation(); - const modal = this.modalCtrl.create('AddonNotesAddPage', { userId: user.id, courseId }); - modal.present(); + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'AddonNotesListPage', { userId: user.id, courseId: courseId }); } }; } diff --git a/src/addon/notifications/notifications.module.ts b/src/addon/notifications/notifications.module.ts index 384901f44..3dbd875f4 100644 --- a/src/addon/notifications/notifications.module.ts +++ b/src/addon/notifications/notifications.module.ts @@ -24,8 +24,10 @@ import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; import { CoreCronDelegate } from '@providers/cron'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; +import { AddonModForumProvider } from '@addon/mod/forum/providers/forum'; // List of providers (without handlers). export const ADDON_NOTIFICATIONS_PROVIDERS: any[] = [ @@ -50,12 +52,44 @@ export class AddonNotificationsModule { cronDelegate: CoreCronDelegate, cronHandler: AddonNotificationsCronHandler, zone: NgZone, appProvider: CoreAppProvider, utils: CoreUtilsProvider, sitesProvider: CoreSitesProvider, notificationsProvider: AddonNotificationsProvider, localNotifications: CoreLocalNotificationsProvider, - linkHelper: CoreContentLinksHelperProvider, pushNotificationsDelegate: AddonPushNotificationsDelegate) { + linkHelper: CoreContentLinksHelperProvider, pushNotificationsDelegate: AddonPushNotificationsDelegate, + urlUtils: CoreUrlUtilsProvider, forumProvider: AddonModForumProvider) { + mainMenuDelegate.registerHandler(mainMenuHandler); settingsDelegate.registerHandler(settingsHandler); cronDelegate.register(cronHandler); const notificationClicked = (notification: any): void => { + + // Temporary fix to make forum notifications work. This will be improved in next release. + if (notification.moodlecomponent == 'mod_forum' && notification.name == 'posts') { + sitesProvider.isFeatureDisabled('CoreCourseModuleDelegate_AddonModForum', notification.site).then((disabled) => { + if (disabled) { + // Forum is disabled, stop. + return; + } + + const contextUrlParams = urlUtils.extractUrlParams(notification.contexturl), + pageParams: any = { + courseId: Number(notification.courseid), + discussionId: Number(contextUrlParams.d), + }; + + if (contextUrlParams.urlHash) { + pageParams.postId = Number(contextUrlParams.urlHash.replace('p', '')); + } + + forumProvider.invalidateDiscussionPosts(pageParams.discussionId).catch(() => { + // Ignore errors. + }).then(() => { + linkHelper.goInSite(undefined, 'AddonModForumDiscussionPage', pageParams, notification.site); + }); + }); + } else { + goToNotifications(notification); + } + }; + const goToNotifications = (notification: any): void => { sitesProvider.isFeatureDisabled('CoreMainMenuDelegate_AddonNotifications', notification.site).then((disabled) => { if (disabled) { // Notifications are disabled, stop. diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts index 0d6c03283..df36c3bed 100644 --- a/src/addon/pushnotifications/providers/pushnotifications.ts +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -13,14 +13,16 @@ // limitations under the License. import { Injectable, NgZone } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { Platform } from 'ionic-angular'; import { Badge } from '@ionic-native/badge'; import { Push, PushObject, PushOptions } from '@ionic-native/push'; import { Device } from '@ionic-native/device'; +import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreInitDelegate } from '@providers/init'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreSitesFactoryProvider } from '@providers/sites-factory'; import { AddonPushNotificationsDelegate } from './delegate'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -28,6 +30,56 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreConfigProvider } from '@providers/config'; import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../../../configconstants'; +import { ILocalNotification } from '@ionic-native/local-notifications'; +import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { CoreSite } from '@classes/site'; + +/** + * Data needed to register a device in a Moodle site. + */ +export interface AddonPushNotificationsRegisterData { + /** + * App ID. + * @type {string} + */ + appid: string; + + /** + * Device UUID. + * @type {string} + */ + uuid: string; + + /** + * Device name. + * @type {string} + */ + name: string; + + /** + * Device model. + * @type {string} + */ + model: string; + + /** + * Device platform. + * @type {string} + */ + platform: string; + + /** + * Device version. + * @type {string} + */ + version: string; + + /** + * Push ID. + * @type {string} + */ + pushid: string; +} /** * Service to handle push notifications. @@ -36,12 +88,14 @@ import { CoreConfigConstants } from '../../../configconstants'; export class AddonPushNotificationsProvider { protected logger; protected pushID: string; - protected appDB: any; + protected appDB: SQLiteDB; static COMPONENT = 'AddonPushNotificationsProvider'; // Variables for database. static BADGE_TABLE = 'addon_pushnotifications_badge'; - protected tablesSchema = [ + static PENDING_UNREGISTER_TABLE = 'addon_pushnotifications_pending_unregister'; + static REGISTERED_DEVICES_TABLE = 'addon_pushnotifications_registered_devices'; + protected appTablesSchema: SQLiteDBTableSchema[] = [ { name: AddonPushNotificationsProvider.BADGE_TABLE, columns: [ @@ -59,18 +113,91 @@ export class AddonPushNotificationsProvider { } ], primaryKeys: ['siteid', 'addon'] + }, + { + name: AddonPushNotificationsProvider.PENDING_UNREGISTER_TABLE, + columns: [ + { + name: 'siteid', + type: 'TEXT', + primaryKey: true + }, + { + name: 'siteurl', + type: 'TEXT' + }, + { + name: 'token', + type: 'TEXT' + }, + { + name: 'info', + type: 'TEXT' + } + ] } ]; + protected siteSchema: CoreSiteSchema = { + name: 'AddonPushNotificationsProvider', + version: 1, + tables: [ + { + name: AddonPushNotificationsProvider.REGISTERED_DEVICES_TABLE, + columns: [ + { + name: 'appid', + type: 'TEXT', + }, + { + name: 'uuid', + type: 'TEXT' + }, + { + name: 'name', + type: 'TEXT' + }, + { + name: 'model', + type: 'TEXT' + }, + { + name: 'platform', + type: 'TEXT' + }, + { + name: 'version', + type: 'TEXT' + }, + { + name: 'pushid', + type: 'TEXT' + }, + ], + primaryKeys: ['appid', 'uuid'] + } + ], + }; constructor(logger: CoreLoggerProvider, protected appProvider: CoreAppProvider, private initDelegate: CoreInitDelegate, protected pushNotificationsDelegate: AddonPushNotificationsDelegate, protected sitesProvider: CoreSitesProvider, private badge: Badge, private localNotificationsProvider: CoreLocalNotificationsProvider, private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, private push: Push, private configProvider: CoreConfigProvider, private device: Device, private zone: NgZone, - private translate: TranslateService) { + private translate: TranslateService, private platform: Platform, private sitesFactory: CoreSitesFactoryProvider) { this.logger = logger.getInstance('AddonPushNotificationsProvider'); this.appDB = appProvider.getDB(); - this.appDB.createTablesFromSchema(this.tablesSchema); + this.appDB.createTablesFromSchema(this.appTablesSchema); + this.sitesProvider.registerSiteSchema(this.siteSchema); + + platform.ready().then(() => { + // Create the default channel. + this.createDefaultChannel(); + + translate.onLangChange.subscribe((event: any) => { + // Update the channel name. + this.createDefaultChannel(); + }); + }); } /** @@ -85,6 +212,25 @@ export class AddonPushNotificationsProvider { }); } + /** + * Create the default push channel. It is used to change the name. + * + * @return {Promise} Promise resolved when done. + */ + protected createDefaultChannel(): Promise { + if (!this.platform.is('android')) { + return Promise.resolve(); + } + + return this.push.createChannel({ + id: 'PushPluginChannel', + description: this.translate.instant('core.misc'), + importance: 4 + }).catch((error) => { + this.logger.error('Error changing push channel name', error); + }); + } + /** * Returns options for push notifications based on device. * @@ -94,8 +240,8 @@ export class AddonPushNotificationsProvider { return this.configProvider.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true).then((soundEnabled) => { return { android: { - senderID: CoreConfigConstants.gcmpn, - sound: !!soundEnabled + sound: !!soundEnabled, + icon: 'smallicon' }, ios: { alert: 'true', @@ -118,6 +264,23 @@ export class AddonPushNotificationsProvider { return this.pushID; } + /** + * Get data to register the device in Moodle. + * + * @return {AddonPushNotificationsRegisterData} Data. + */ + protected getRegisterData(): AddonPushNotificationsRegisterData { + return { + appid: CoreConfigConstants.app_id, + name: this.device.manufacturer || '', + model: this.device.model, + platform: this.device.platform + '-fcm', + version: this.device.version, + pushid: this.pushID, + uuid: this.device.uuid + }; + } + /** * Get Sitebadge counter from the database. * @@ -153,18 +316,9 @@ export class AddonPushNotificationsProvider { if (this.utils.isTrueOrOne(data.foreground)) { // If the app is in foreground when the notification is received, it's not shown. Let's show it ourselves. if (this.localNotificationsProvider.isAvailable()) { - const localNotif = { + const localNotif: ILocalNotification = { id: 1, - at: new Date(), - channelParams: { - channelID: 'notifications', - channelName: this.translate.instant('addon.notifications.notifications'), - importance: 4 // IMPORTANCE_HIGH - }, - data: { - notif: data.notif, - site: data.site - }, + data: data, title: '', text: '' }, @@ -196,9 +350,6 @@ export class AddonPushNotificationsProvider { }); } else { // The notification was clicked. - // For compatibility with old push plugin implementation we'll merge all the notification data in a single object. - data.title = notification.title; - data.message = notification.message; this.notificationClicked(data); } }); @@ -207,10 +358,10 @@ export class AddonPushNotificationsProvider { /** * Unregisters a device from a certain Moodle site. * - * @param {any} site Site to unregister from. - * @return {Promise} Promise resolved when device is unregistered. + * @param {CoreSite} site Site to unregister from. + * @return {Promise} Promise resolved when device is unregistered. */ - unregisterDeviceOnMoodle(site: any): Promise { + unregisterDeviceOnMoodle(site: CoreSite): Promise { if (!site || !this.appProvider.isMobile()) { return Promise.reject(null); } @@ -226,6 +377,34 @@ export class AddonPushNotificationsProvider { if (!response || !response.removed) { return Promise.reject(null); } + + const promises = []; + + // Remove the device from the local DB. + promises.push(site.getDb().deleteRecords(AddonPushNotificationsProvider.REGISTERED_DEVICES_TABLE, + this.getRegisterData())); + + // Remove pending unregisters for this site. + promises.push(this.appDB.deleteRecords(AddonPushNotificationsProvider.PENDING_UNREGISTER_TABLE, {siteid: site.id})); + + return Promise.all(promises).catch(() => { + // Ignore errors. + }); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, can't unregister. + return Promise.reject(error); + } + + // Store the pending unregister so it's retried again later. + return this.appDB.insertRecord(AddonPushNotificationsProvider.PENDING_UNREGISTER_TABLE, { + siteid: site.id, + siteurl: site.getURL(), + token: site.getToken(), + info: JSON.stringify(site.getInfo()) + }).then(() => { + return Promise.reject(error); + }); }); } @@ -368,28 +547,58 @@ export class AddonPushNotificationsProvider { } /** - * Registers a device on current Moodle site. + * Registers a device on a Moodle site if needed. * - * @return {Promise} Promise resolved when device is registered. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [forceUnregister] Whether to force unregister and register. + * @return {Promise} Promise resolved when device is registered. */ - registerDeviceOnMoodle(): Promise { + registerDeviceOnMoodle(siteId?: string, forceUnregister?: boolean): Promise { this.logger.debug('Register device on Moodle.'); - if (!this.sitesProvider.isLoggedIn() || !this.pushID || !this.appProvider.isMobile()) { + if (!this.pushID || !this.appProvider.isMobile()) { return Promise.reject(null); } - const data = { - appid: CoreConfigConstants.app_id, - name: this.device.manufacturer || '', - model: this.device.model, - platform: this.device.platform, - version: this.device.version, - pushid: this.pushID, - uuid: this.device.uuid - }; + const data = this.getRegisterData(); + let result, + site: CoreSite; - return this.sitesProvider.getCurrentSite().write('core_user_add_user_device', data); + return this.sitesProvider.getSite(siteId).then((s) => { + site = s; + + if (forceUnregister) { + return {unregister: true, register: true}; + } else { + // Check if the device is already registered. + return this.shouldRegister(data, site); + } + }).then((res) => { + result = res; + + if (result.unregister) { + // Unregister the device first. + return this.unregisterDeviceOnMoodle(site).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + if (result.register) { + // Now register the device. + return site.write('core_user_add_user_device', this.utils.clone(data)).then((response) => { + // Insert the device in the local DB. + return site.getDb().insertRecord(AddonPushNotificationsProvider.REGISTERED_DEVICES_TABLE, data) + .catch((error) => { + // Ignore errors. + }); + }); + } + }).finally(() => { + // Remove pending unregisters for this site. + this.appDB.deleteRecords(AddonPushNotificationsProvider.PENDING_UNREGISTER_TABLE, {siteid: site.id}).catch(() => { + // Ignore errors. + }); + }); } /** @@ -407,6 +616,38 @@ export class AddonPushNotificationsProvider { }); } + /** + * Retry pending unregisters. + * + * @param {string} [siteId] If defined, retry only for that site if needed. Otherwise, retry all pending unregisters. + * @return {Promise} Promise resolved when done. + */ + retryUnregisters(siteId?: string): Promise { + let promise; + + if (siteId) { + // Check if the site has a pending unregister. + promise = this.appDB.getRecords(AddonPushNotificationsProvider.REGISTERED_DEVICES_TABLE, {siteid: siteId}); + } else { + // Get all pending unregisters. + promise = this.appDB.getAllRecords(AddonPushNotificationsProvider.PENDING_UNREGISTER_TABLE); + } + + return promise.then((results) => { + const promises = []; + + results.forEach((result) => { + // Create a temporary site to unregister. + const tmpSite = this.sitesFactory.makeSite(result.siteid, result.siteurl, result.token, + this.textUtils.parseJSON(result.info, {})); + + promises.push(this.unregisterDeviceOnMoodle(tmpSite)); + }); + + return Promise.all(promises); + }); + } + /** * Save the addon/site badgecounter on the database. * @@ -428,4 +669,58 @@ export class AddonPushNotificationsProvider { return value; }); } + + /** + * Check if device should be registered (and unregistered first). + * + * @param {AddonPushNotificationsRegisterData} data Data of the device. + * @param {CoreSite} site Site to use. + * @return {Promise<{register: boolean, unregister: boolean}>} Promise resolved with booleans: whether to register/unregister. + */ + protected shouldRegister(data: AddonPushNotificationsRegisterData, site: CoreSite) + : Promise<{register: boolean, unregister: boolean}> { + + // Check if the device is already registered. + return site.getDb().getRecords(AddonPushNotificationsProvider.REGISTERED_DEVICES_TABLE, { + appid: data.appid, + uuid: data.uuid + }).catch(() => { + // Ignore errors. + return []; + }).then((records: AddonPushNotificationsRegisterData[]) => { + let isStored = false, + versionOrPushChanged = false; + + records.forEach((record) => { + if (record.name == data.name && record.model == data.model && record.platform == data.platform) { + if (record.version == data.version && record.pushid == data.pushid) { + // The device is already stored. + isStored = true; + } else { + // The version or pushid has changed. + versionOrPushChanged = true; + } + } + }); + + if (isStored) { + // The device has already been registered, no need to register it again. + return { + register: false, + unregister: false + }; + } else if (versionOrPushChanged) { + // This data can be updated by calling register WS, no need to call unregister. + return { + register: true, + unregister: false + }; + } else { + return { + register: true, + unregister: true + }; + } + }); + } } diff --git a/src/addon/pushnotifications/providers/register-cron-handler.ts b/src/addon/pushnotifications/providers/register-cron-handler.ts new file mode 100644 index 000000000..fd3ea87ff --- /dev/null +++ b/src/addon/pushnotifications/providers/register-cron-handler.ts @@ -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 { CoreCronHandler } from '@providers/cron'; +import { AddonPushNotificationsProvider } from './pushnotifications'; + +/** + * Cron handler to force a register on a Moodle site when a site is manually synchronized. + */ +@Injectable() +export class AddonPushNotificationsRegisterCronHandler implements CoreCronHandler { + name = 'AddonPushNotificationsRegisterCronHandler'; + + constructor(private pushNotificationsProvider: AddonPushNotificationsProvider) {} + + /** + * Check whether the sync can be executed manually. Call isSync if not defined. + * + * @return {boolean} Whether the sync can be executed manually. + */ + canManualSync(): boolean { + return true; // Execute the handler when the site is manually synchronized. + } + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + if (!siteId) { + // It's not a specific site, don't do anything. + return Promise.resolve(); + } + + // Register the device again. + return this.pushNotificationsProvider.registerDeviceOnMoodle(siteId, true); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 86400000; // 1 day. We won't do anything with automatic execution, so use a big number. + } + + /** + * Check whether it's a synchronization process or not. True if not defined. + * + * @return {boolean} Whether it's a synchronization process or not. + */ + isSync(): boolean { + return false; + } +} diff --git a/src/addon/pushnotifications/providers/unregister-cron-handler.ts b/src/addon/pushnotifications/providers/unregister-cron-handler.ts new file mode 100644 index 000000000..548d44bd0 --- /dev/null +++ b/src/addon/pushnotifications/providers/unregister-cron-handler.ts @@ -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 { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@providers/cron'; +import { AddonPushNotificationsProvider } from './pushnotifications'; + +/** + * Cron handler to retry pending unregisters. + */ +@Injectable() +export class AddonPushNotificationsUnregisterCronHandler implements CoreCronHandler { + name = 'AddonPushNotificationsUnregisterCronHandler'; + + constructor(private pushNotificationsProvider: AddonPushNotificationsProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.pushNotificationsProvider.retryUnregisters(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 300000; + } +} diff --git a/src/addon/pushnotifications/pushnotifications.module.ts b/src/addon/pushnotifications/pushnotifications.module.ts index 6de574ecc..d36a0ce43 100644 --- a/src/addon/pushnotifications/pushnotifications.module.ts +++ b/src/addon/pushnotifications/pushnotifications.module.ts @@ -16,6 +16,9 @@ import { NgModule } from '@angular/core'; import { Platform } from 'ionic-angular'; import { AddonPushNotificationsProvider } from './providers/pushnotifications'; import { AddonPushNotificationsDelegate } from './providers/delegate'; +import { AddonPushNotificationsRegisterCronHandler } from './providers/register-cron-handler'; +import { AddonPushNotificationsUnregisterCronHandler } from './providers/unregister-cron-handler'; +import { CoreCronDelegate } from '@providers/cron'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; @@ -34,16 +37,24 @@ export const ADDON_PUSHNOTIFICATIONS_PROVIDERS: any[] = [ ], providers: [ AddonPushNotificationsProvider, - AddonPushNotificationsDelegate + AddonPushNotificationsDelegate, + AddonPushNotificationsRegisterCronHandler, + AddonPushNotificationsUnregisterCronHandler ] }) export class AddonPushNotificationsModule { constructor(platform: Platform, pushNotificationsProvider: AddonPushNotificationsProvider, eventsProvider: CoreEventsProvider, localNotificationsProvider: CoreLocalNotificationsProvider, loggerProvider: CoreLoggerProvider, - updateManager: CoreUpdateManagerProvider) { + updateManager: CoreUpdateManagerProvider, cronDelegate: CoreCronDelegate, + registerCronHandler: AddonPushNotificationsRegisterCronHandler, + unregisterCronHandler: AddonPushNotificationsUnregisterCronHandler) { const logger = loggerProvider.getInstance('AddonPushNotificationsModule'); + // Register the handlers. + cronDelegate.register(registerCronHandler); + cronDelegate.register(unregisterCronHandler); + // Register device on GCM or APNS server. platform.ready().then(() => { pushNotificationsProvider.registerDevice(); diff --git a/src/addon/qtype/calculated/calculated.module.ts b/src/addon/qtype/calculated/calculated.module.ts index fed580b35..a819b755e 100644 --- a/src/addon/qtype/calculated/calculated.module.ts +++ b/src/addon/qtype/calculated/calculated.module.ts @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { AddonQtypeCalculatedHandler } from './providers/handler'; import { AddonQtypeCalculatedComponent } from './component/calculated'; @@ -27,6 +28,7 @@ import { AddonQtypeCalculatedComponent } from './component/calculated'; imports: [ IonicModule, TranslateModule.forChild(), + CoreComponentsModule, CoreDirectivesModule ], providers: [ diff --git a/src/addon/qtype/calculated/component/addon-qtype-calculated.html b/src/addon/qtype/calculated/component/addon-qtype-calculated.html index 144d02c03..e2ea63c89 100644 --- a/src/addon/qtype/calculated/component/addon-qtype-calculated.html +++ b/src/addon/qtype/calculated/component/addon-qtype-calculated.html @@ -8,8 +8,9 @@ - - + + {{ 'addon.mod_quiz.answercolon' | translate }} + @@ -18,7 +19,7 @@ - + @@ -27,6 +28,7 @@ + @@ -54,9 +56,9 @@
-

{{option.text}}

+
- +
diff --git a/src/addon/qtype/calculated/component/calculated.scss b/src/addon/qtype/calculated/component/calculated.scss new file mode 100644 index 000000000..eb53fdb3d --- /dev/null +++ b/src/addon/qtype/calculated/component/calculated.scss @@ -0,0 +1,5 @@ +ion-app.app-root addon-qtype-calculated { + ion-col .select-disabled { + @include margin(0, 20px, 0, 0); + } +} \ No newline at end of file diff --git a/src/addon/qtype/ddimageortext/component/ddimageortext.scss b/src/addon/qtype/ddimageortext/component/ddimageortext.scss index 48c69d82e..57681ec2e 100644 --- a/src/addon/qtype/ddimageortext/component/ddimageortext.scss +++ b/src/addon/qtype/ddimageortext/component/ddimageortext.scss @@ -29,30 +29,12 @@ addon-qtype-ddimageortext { zoom: 1; } - .group1 { - background-color: $white; - } - .group2 { - background-color: $blue-light; - } - .group3 { - background-color: #DCDCDC; - } - .group4 { - background-color: #D8BFD8; - } - .group5 { - background-color: #87CEFA; - } - .group6 { - background-color: #DAA520; - } - .group7 { - background-color: #FFD700; - } - .group8 { - background-color: #F0E68C; + @for $i from 0 to length($core-dd-question-colors) { + .group#{$i + 1} { + background: nth($core-dd-question-colors, $i + 1); + } } + .drag { border: 1px solid $gray-darker; cursor: pointer; @@ -96,6 +78,6 @@ addon-qtype-ddimageortext { } .drag.beingdragged { z-index: 3; - box-shadow: 3px 3px 4px $gray-darker; + box-shadow: $core-dd-question-selected-shadow; } } diff --git a/src/addon/qtype/ddmarker/classes/ddmarker.ts b/src/addon/qtype/ddmarker/classes/ddmarker.ts index 46cb4909f..f303d5c1b 100644 --- a/src/addon/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addon/qtype/ddmarker/classes/ddmarker.ts @@ -290,18 +290,13 @@ export class AddonQtypeDdMarkerQuestion { * @param {string} shape Name of the shape of the drop zone (circle, rectangle, polygon). * @param {string} coords Coordinates of the shape. * @param {string} colour Colour of the shape. - * @param {boolean} link Whether the marker should have a link in it. */ - drawDropZone(dropZoneNo: number, markerText: string, shape: string, coords: string, colour: string, link: boolean): void { + drawDropZone(dropZoneNo: number, markerText: string, shape: string, coords: string, colour: string): void { let existingMarkerText: HTMLElement; const markerTexts = this.doc.markerTexts(); // Check if there is already a marker text for this drop zone. - if (link) { - existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo + ' a'); - } else { - existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo); - } + existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo); if (existingMarkerText) { // Marker text already exists. Update it or remove it if empty. @@ -316,12 +311,7 @@ export class AddonQtypeDdMarkerQuestion { span = document.createElement('span'); span.className = classNames; - - if (link) { - span.innerHTML = '' + markerText + ''; - } else { - span.innerHTML = markerText; - } + span.innerHTML = markerText; markerTexts.appendChild(span); } @@ -802,7 +792,7 @@ export class AddonQtypeDdMarkerQuestion { dropZone = this.dropZones[dropZoneNo], dzNo = Number(dropZoneNo); - this.drawDropZone(dzNo, dropZone.markertext, dropZone.shape, dropZone.coords, colourForDropZone, true); + this.drawDropZone(dzNo, dropZone.markertext, dropZone.shape, dropZone.coords, colourForDropZone); } } } diff --git a/src/addon/qtype/ddmarker/component/ddmarker.scss b/src/addon/qtype/ddmarker/component/ddmarker.scss index e53f1baec..d9197d122 100644 --- a/src/addon/qtype/ddmarker/component/ddmarker.scss +++ b/src/addon/qtype/ddmarker/component/ddmarker.scss @@ -33,7 +33,7 @@ addon-qtype-ddmarker { .dragitem.beingdragged .markertext { z-index: 5; - box-shadow: 3px 3px 4px $gray-darker; + box-shadow: $core-dd-question-selected-shadow; } .dragitems .draghome { margin: 10px; diff --git a/src/addon/qtype/ddwtos/component/ddwtos.scss b/src/addon/qtype/ddwtos/component/ddwtos.scss index 47ae4151e..a594f55fa 100644 --- a/src/addon/qtype/ddwtos/component/ddwtos.scss +++ b/src/addon/qtype/ddwtos/component/ddwtos.scss @@ -40,10 +40,11 @@ addon-qtype-ddwtos { .drag { z-index: 2; border-radius: 5px; + line-height: 25px; } .drag.selected { z-index: 3; - box-shadow: 3px 3px 4px $gray-darker; + box-shadow: $core-dd-question-selected-shadow; } .drop.selected { @@ -73,29 +74,10 @@ addon-qtype-ddwtos { background-color: $green-light; } - .group1 { - background-color: $white; - } - .group2 { - background-color: #DCDCDC; - } - .group3 { - background-color: $blue-light; - } - .group4 { - background-color: #D8BFD8; - } - .group5 { - background-color: #87CEFA; - } - .group6 { - background-color: #DAA520; - } - .group7 { - background-color: #FFD700; - } - .group8 { - background-color: #F0E68C; + @for $i from 0 to length($core-dd-question-colors) { + .group#{$i + 1} { + background: nth($core-dd-question-colors, $i + 1); + } } sub, sup { diff --git a/src/addon/qtype/gapselect/component/gapselect.scss b/src/addon/qtype/gapselect/component/gapselect.scss index b467d9351..53ceeba30 100644 --- a/src/addon/qtype/gapselect/component/gapselect.scss +++ b/src/addon/qtype/gapselect/component/gapselect.scss @@ -1,5 +1,5 @@ // Style gapselect content a bit. All these styles are copied from Moodle. -addon-qtype-gapselect { +ion-app.app-root addon-qtype-gapselect { p { margin: 0 0 .5em; } diff --git a/src/addon/qtype/match/component/addon-qtype-match.html b/src/addon/qtype/match/component/addon-qtype-match.html index b392ef3b4..95bed5ddc 100644 --- a/src/addon/qtype/match/component/addon-qtype-match.html +++ b/src/addon/qtype/match/component/addon-qtype-match.html @@ -1,19 +1,22 @@
-

+
- - + +

- + - + {{option.label}} + + + diff --git a/src/addon/qtype/match/component/match.scss b/src/addon/qtype/match/component/match.scss new file mode 100644 index 000000000..89e49a50b --- /dev/null +++ b/src/addon/qtype/match/component/match.scss @@ -0,0 +1,10 @@ +ion-app.app-root addon-qtype-match { + ion-col .select-disabled { + @include margin(0, 20px, 0, 0); + } + + .core-correct-icon { + bottom: 50%; + margin-bottom: -7px; + } +} diff --git a/src/addon/qtype/match/match.module.ts b/src/addon/qtype/match/match.module.ts index b4b56f334..f140418a4 100644 --- a/src/addon/qtype/match/match.module.ts +++ b/src/addon/qtype/match/match.module.ts @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { AddonQtypeMatchHandler } from './providers/handler'; import { AddonQtypeMatchComponent } from './component/match'; @@ -27,6 +28,7 @@ import { AddonQtypeMatchComponent } from './component/match'; imports: [ IonicModule, TranslateModule.forChild(), + CoreComponentsModule, CoreDirectivesModule ], providers: [ diff --git a/src/addon/qtype/multianswer/component/addon-qtype-multianswer.html b/src/addon/qtype/multianswer/component/addon-qtype-multianswer.html index 899f1c34e..74bc91da4 100644 --- a/src/addon/qtype/multianswer/component/addon-qtype-multianswer.html +++ b/src/addon/qtype/multianswer/component/addon-qtype-multianswer.html @@ -1,5 +1,5 @@
-

+
diff --git a/src/addon/qtype/multianswer/component/multianswer.scss b/src/addon/qtype/multianswer/component/multianswer.scss index 5d116515e..f63e3e5ca 100644 --- a/src/addon/qtype/multianswer/component/multianswer.scss +++ b/src/addon/qtype/multianswer/component/multianswer.scss @@ -22,12 +22,12 @@ addon-qtype-multianswer { } input, select { + @include border-radius(4px); + display: inline-block; - border: 1px solid #ccc; - padding: 4px 6px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; + border: 1px solid $gray-dark; + padding: 6px 8px; + @include margin-horizontal(2px); margin-bottom: 10px; } diff --git a/src/addon/qtype/multichoice/component/addon-qtype-multichoice.html b/src/addon/qtype/multichoice/component/addon-qtype-multichoice.html index a39aaaa2e..c41b376b9 100644 --- a/src/addon/qtype/multichoice/component/addon-qtype-multichoice.html +++ b/src/addon/qtype/multichoice/component/addon-qtype-multichoice.html @@ -1,18 +1,20 @@
-

-

+ +
- - + + -

+
- + + + @@ -21,12 +23,14 @@
- + -

+
- + + +
diff --git a/src/addon/qtype/multichoice/component/multichoice.scss b/src/addon/qtype/multichoice/component/multichoice.scss new file mode 100644 index 000000000..6312d0e50 --- /dev/null +++ b/src/addon/qtype/multichoice/component/multichoice.scss @@ -0,0 +1,13 @@ +ion-app.app-root addon-qtype-multichoice { + .core-correct-icon { + bottom: 50%; + margin-bottom: -7px; + } + + .specificfeedback { + background-color: $core-question-feedback-color-bg; + color: $core-question-feedback-color; + display: inline; + padding: 0 .7em; + } +} diff --git a/src/addon/qtype/multichoice/multichoice.module.ts b/src/addon/qtype/multichoice/multichoice.module.ts index c3d591ba7..a0e36e755 100644 --- a/src/addon/qtype/multichoice/multichoice.module.ts +++ b/src/addon/qtype/multichoice/multichoice.module.ts @@ -17,6 +17,7 @@ import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; import { AddonQtypeMultichoiceHandler } from './providers/handler'; import { AddonQtypeMultichoiceComponent } from './component/multichoice'; @@ -27,6 +28,7 @@ import { AddonQtypeMultichoiceComponent } from './component/multichoice'; imports: [ IonicModule, TranslateModule.forChild(), + CoreComponentsModule, CoreDirectivesModule ], providers: [ diff --git a/src/addon/qtype/shortanswer/component/addon-qtype-shortanswer.html b/src/addon/qtype/shortanswer/component/addon-qtype-shortanswer.html index 550e65863..42febebf0 100644 --- a/src/addon/qtype/shortanswer/component/addon-qtype-shortanswer.html +++ b/src/addon/qtype/shortanswer/component/addon-qtype-shortanswer.html @@ -1,7 +1,11 @@
- -

+ + - - + + {{ 'addon.mod_quiz.answercolon' | translate }} + + + +
diff --git a/src/addon/qtype/shortanswer/component/shortanswer.scss b/src/addon/qtype/shortanswer/component/shortanswer.scss new file mode 100644 index 000000000..02c8bd099 --- /dev/null +++ b/src/addon/qtype/shortanswer/component/shortanswer.scss @@ -0,0 +1,18 @@ +addon-qtype-shortanswer { + .addon-qtype-shortanswer-text { + ion-label.label { + margin-bottom: 0; + } + + input { + @include placeholder($text-input-placeholder-color); + @include appearance(none); + @include border-radius(4px); + display: inline-block; + border: 1px solid $gray-dark; + padding: 6px 8px; + @include margin-horizontal(2px); + margin-bottom: 10px; + } + } +} diff --git a/src/addon/qtype/shortanswer/shortanswer.module.ts b/src/addon/qtype/shortanswer/shortanswer.module.ts index 58497545d..313a23b75 100644 --- a/src/addon/qtype/shortanswer/shortanswer.module.ts +++ b/src/addon/qtype/shortanswer/shortanswer.module.ts @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { AddonQtypeShortAnswerHandler } from './providers/handler'; import { AddonQtypeShortAnswerComponent } from './component/shortanswer'; @@ -27,6 +28,7 @@ import { AddonQtypeShortAnswerComponent } from './component/shortanswer'; imports: [ IonicModule, TranslateModule.forChild(), + CoreComponentsModule, CoreDirectivesModule ], providers: [ diff --git a/src/addon/qtype/truefalse/providers/handler.ts b/src/addon/qtype/truefalse/providers/handler.ts index 59c47fdff..3be03abde 100644 --- a/src/addon/qtype/truefalse/providers/handler.ts +++ b/src/addon/qtype/truefalse/providers/handler.ts @@ -84,4 +84,20 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); } + + /** + * Prepare and add to answers the data to send to server based in the input. Return promise if async. + * + * @param {any} question Question. + * @param {any} answers The answers retrieved from the form. Prepared answers must be stored in this object. + * @param {boolean} [offline] Whether the data should be saved in offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Return a promise resolved when done if async, void if sync. + */ + prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + if (question && typeof answers[question.optionsName] != 'undefined' && !answers[question.optionsName]) { + // The user hasn't answered. Delete the answer to prevent marking one of the answers automatically. + delete answers[question.optionsName]; + } + } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f368628ec..dec433beb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -51,11 +51,17 @@ export class MoodleMobileApp implements OnInit { keyboard.hideFormAccessoryBar(false); - let desktopClass = this.appProvider.isDesktop() ? 'platform-desktop' : ''; - desktopClass += this.appProvider.isMac() ? ' platform-mac' : ''; - desktopClass += this.appProvider.isLinux() ? ' platform-linux' : ''; - desktopClass += this.appProvider.isWindows() ? ' platform-windows' : ''; - desktopClass != '' ? app.setElementClass(desktopClass, true) : false; + if (this.appProvider.isDesktop()) { + app.setElementClass('platform-desktop', true); + + if (this.appProvider.isMac()) { + app.setElementClass('platform-mac', true); + } else if (this.appProvider.isLinux()) { + app.setElementClass('platform-linux', true); + } else if (this.appProvider.isWindows()) { + app.setElementClass('platform-windows', true); + } + } }); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 83003549f..fbcdc4cb5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,6 +31,7 @@ import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { MoodleMobileApp } from './app.component'; import { CoreInterceptor } from '@classes/interceptor'; import { CorePageTransition } from '@classes/page-transition'; +import { CoreModalLateralTransition } from '@classes/modal-lateral-transition'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDbProvider } from '@providers/db'; import { CoreAppProvider } from '@providers/app'; @@ -78,9 +79,11 @@ import { CoreCompileModule } from '@core/compile/compile.module'; import { CoreQuestionModule } from '@core/question/question.module'; import { CoreCommentsModule } from '@core/comments/comments.module'; import { CoreBlockModule } from '@core/block/block.module'; +import { CoreRatingModule } from '@core/rating/rating.module'; // Addon modules. import { AddonBadgesModule } from '@addon/badges/badges.module'; +import { AddonBlogModule } from '@addon/blog/blog.module'; import { AddonCalendarModule } from '@addon/calendar/calendar.module'; import { AddonCompetencyModule } from '@addon/competency/competency.module'; import { AddonCourseCompletionModule } from '@addon/coursecompletion/coursecompletion.module'; @@ -197,7 +200,9 @@ export const CORE_PROVIDERS: any[] = [ CoreQuestionModule, CoreCommentsModule, CoreBlockModule, + CoreRatingModule, AddonBadgesModule, + AddonBlogModule, AddonCalendarModule, AddonCompetencyModule, AddonCourseCompletionModule, @@ -319,6 +324,7 @@ export class AppModule { // Set transition animation. config.setTransition('core-page-transition', CorePageTransition); + config.setTransition('core-modal-lateral-transition', CoreModalLateralTransition); // Decorate ion-content. this.decorateIonContent(); diff --git a/src/app/app.scss b/src/app/app.scss index fa548765c..f9c07d4f9 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -73,6 +73,10 @@ ion-app.app-root { } } + .has-refresher > .scroll-content { + border-top: 0 !important; + } + // Define an alternative way to set a heading in an item without using a heading tag. // This is done for accessibility reasons when a heading is semantically incorrect. .item .item-heading { @@ -99,6 +103,10 @@ ion-app.app-root { border: 0; } + .core-nav-item-selected, .item.core-nav-item-selected { + @include core-selected-item($core-splitview-selected); + } + // Recover borders on items inside cards. .card.with-borders .core-as-item, .core-as-item { @@ -130,6 +138,8 @@ ion-app.app-root { .core-module-icon { width: auto; + max-width: 24px; + max-height: 24px; } .core-button-spinner { @@ -372,6 +382,21 @@ ion-app.app-root { .select-icon .select-icon-inner { color: $core-select-placeholder-color; } + + &.select-disabled, .select-icon .select-icon-inner { + color: $text-color; + } + @each $color-name, $color-base, $color-contrast in get-colors($colors) { + &.select-md-#{$color-name}, + &.select-ios-#{$color-name}, + &.select-wp-#{$color-name} { + color: $color-base; + + .select-icon .select-icon-inner { + color: $color-base; + } + } + } } ion-select.core-button-select, @@ -448,8 +473,31 @@ ion-app.app-root { // Question. // ------------------------- + .core-correct-icon { + padding: 0 ($content-padding / 2); + position: absolute; + @include position(null, 0, $content-padding / 2, null); + margin-top: 0; + margin-bottom: 0; + } - .core-question-answer-correct, + + .core-question-answer-correct { + color: $core-question-correct-color; + } + + .core-question-answer-incorrect { + color: $core-question-incorrect-color; + } + + input, select { + &.core-question-answer-correct, &.core-question-answer-incorrect { + background-color: $gray-lighter; + color: $text-color; + } + } + + .core-question-correct, .core-question-comment { color: $core-question-correct-color; background-color: $core-question-correct-color-bg; @@ -465,7 +513,6 @@ ion-app.app-root { } } - .core-question-answer-incorrect, .core-question-incorrect { color: $core-question-incorrect-color; background-color: $core-question-incorrect-color-bg; @@ -527,7 +574,8 @@ ion-app.app-root { .core-question-incorrect { background-color: $core-question-state-incorrect-color; } - .core-question-answersaved { + .core-question-answersaved, + .core-question-requiresgrading { color: $text-color; background-color: $core-question-saved-color-bg; } @@ -601,7 +649,7 @@ ion-app.app-root { @include padding(null, null, null, 52px); position: relative; - ion-icon { + > ion-icon { color: $color-base; position: absolute; @include position(0, null, null, 16px) @@ -620,6 +668,22 @@ ion-app.app-root { } } + .core-#{$color-name}-item.item-input { + border-bottom: 0 !important; + + &.item-md .item-inner { + @include md-input-highlight($color-base); + } + + &.item-ios .item-inner { + @include ios-input-highlight($color-base); + } + + &.item-wp .item-inner { + border-color: $color-base; + } + } + .core-#{$color-name}-selected-item { @include safe-area-border-start(5px, solid, $color-base); @@ -677,14 +741,29 @@ ion-app.app-root { width: 100%; } - .core-modal-fullscreen { - .modal-wrapper { - position: absolute; - @include position(0 !important, null, null, 0 !important); - display: block; - width: 100% !important; - height: 100% !important; - } + .core-modal-fullscreen .modal-wrapper { + position: absolute; + @include position(0 !important, null, null, 0 !important); + display: block; + width: 100% !important; + height: 100% !important; + } + + @media only screen and (min-height: 400px) and (min-width: 300px) { + .core-modal-lateral { + .modal-wrapper { + position: absolute; + @include position(0 !important, 0 !important, 0 !important, auto); + display: block; + height: 100% !important; + width: auto; + min-width: 300px; + box-shadow: 0 28px 48px rgba(0, 0, 0, 0.4); + } + ion-backdrop { + visibility: visible; + } + } } .has-fab .scroll-content{ @@ -946,7 +1025,9 @@ details summary { pointer-events: auto; } -.icon.fa-graduation-cap{ +.icon.fa-graduation-cap, +.item > .icon.fa, +.item-inner > .icon.fa { font-size: 21px; width: 21px; line-height: 28px; @@ -968,3 +1049,16 @@ ion-modal, .split-pane { contain: size layout style; } + +// Styles for desktop apps only. +ion-app.platform-desktop { + video::-webkit-media-text-track-display { + white-space: normal !important; + } + + &.platform-windows { + video::-webkit-media-text-track-display { + font-size: 0.6em; + } + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index d12017722..8e7ff5128 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -56,8 +56,19 @@ "addon.block_timeline.pluginname": "Timeline", "addon.block_timeline.sortbycourses": "Sort by courses", "addon.block_timeline.sortbydates": "Sort by dates", + "addon.blog.blog": "Blog", + "addon.blog.blogentries": "Blog entries", + "addon.blog.errorloadentries": "Error loading blog entries.", + "addon.blog.linktooriginalentry": "Link to original blog entry", + "addon.blog.noentriesyet": "No visible entries here", + "addon.blog.publishtonoone": "Yourself (draft)", + "addon.blog.publishtosite": "Anyone on this site", + "addon.blog.publishtoworld": "Anyone in the world", + "addon.blog.showonlyyourentries": "Show only your entries", + "addon.blog.siteblogheading": "Site blog", "addon.calendar.calendar": "Calendar", "addon.calendar.calendarevents": "Calendar events", + "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.defaultnotificationtime": "Default notification time", "addon.calendar.errorloadevent": "Error loading event.", "addon.calendar.errorloadevents": "Error loading events.", @@ -65,7 +76,8 @@ "addon.calendar.eventstarttime": "Start time", "addon.calendar.gotoactivity": "Go to activity", "addon.calendar.noevents": "There are no events", - "addon.calendar.notifications": "Notifications", + "addon.calendar.reminders": "Reminders", + "addon.calendar.setnewreminder": "Set a new reminder", "addon.calendar.typecategory": "Category event", "addon.calendar.typeclose": "Close event", "addon.calendar.typecourse": "Course event", @@ -133,6 +145,7 @@ "addon.coursecompletion.criteriarequiredany": "Any criteria below are required", "addon.coursecompletion.inprogress": "In progress", "addon.coursecompletion.manualselfcompletion": "Manual self completion", + "addon.coursecompletion.nottracked": "You are currently not being tracked by completion in this course", "addon.coursecompletion.notyetstarted": "Not yet started", "addon.coursecompletion.pending": "Pending", "addon.coursecompletion.required": "Required", @@ -214,12 +227,15 @@ "addon.messages.unabletomessage": "You are unable to message this user", "addon.messages.unblockuser": "Unblock user", "addon.messages.unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", + "addon.messages.useentertosend": "Use enter to send", + "addon.messages.useentertosenddescdesktop": "If disabled, you can use Ctrl+Enter to send the message.", + "addon.messages.useentertosenddescmac": "If disabled, you can use Cmd+Enter to send the message.", "addon.messages.userwouldliketocontactyou": "{{$a}} would like to contact you", "addon.messages.warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", "addon.messages.warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", "addon.messages.wouldliketocontactyou": "Would like to contact you", "addon.messages.you": "You:", - "addon.messages.youhaveblockeduser": "You have blocked this user in the past", + "addon.messages.youhaveblockeduser": "You have blocked this user.", "addon.messages.yourcontactrequestpending": "Your contact request is pending with {{$a}}", "addon.mod_assign.acceptsubmissionstatement": "Please accept the submission statement.", "addon.mod_assign.addattempt": "Allow another attempt", @@ -328,9 +344,12 @@ "addon.mod_assign_submission_comments.pluginname": "Submission comments", "addon.mod_assign_submission_file.pluginname": "File submissions", "addon.mod_assign_submission_onlinetext.pluginname": "Online text submissions", + "addon.mod_assign_submission_onlinetext.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.", "addon.mod_book.errorchapter": "Error reading chapter of book.", "addon.mod_book.modulenameplural": "Books", + "addon.mod_book.toc": "Table of contents", "addon.mod_chat.beep": "Beep", + "addon.mod_chat.chatreport": "Chat sessions", "addon.mod_chat.currentusers": "Current users", "addon.mod_chat.enterchat": "Click here to enter the chat now", "addon.mod_chat.entermessage": "Enter your message", @@ -342,20 +361,24 @@ "addon.mod_chat.messagebeepsyou": "{{$a}} has just beeped you!", "addon.mod_chat.messageenter": "{{$a}} has just entered this chat", "addon.mod_chat.messageexit": "{{$a}} has left this chat", + "addon.mod_chat.messages": "Messages", "addon.mod_chat.modulenameplural": "Chats", "addon.mod_chat.mustbeonlinetosendmessages": "You must be online to send messages.", "addon.mod_chat.nomessages": "No messages yet", + "addon.mod_chat.nosessionsfound": "No sessions found", "addon.mod_chat.send": "Send", "addon.mod_chat.sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)", + "addon.mod_chat.showincompletesessions": "Show incomplete sessions", "addon.mod_chat.talk": "Talk", + "addon.mod_chat.viewreport": "View past chat sessions", "addon.mod_choice.cannotsubmit": "Sorry, there was a problem submitting your choice. Please try again.", "addon.mod_choice.choiceoptions": "Choice options", "addon.mod_choice.errorgetchoice": "Error getting choice data.", - "addon.mod_choice.expired": "Sorry, this activity closed on {{$a}} and is no longer available", + "addon.mod_choice.expired": "This activity closed on {{$a}}.", "addon.mod_choice.full": "(Full)", "addon.mod_choice.modulenameplural": "Choices", "addon.mod_choice.noresultsviewable": "The results are not currently viewable.", - "addon.mod_choice.notopenyet": "Sorry, this activity is not available until {{$a}}", + "addon.mod_choice.notopenyet": "This activity is not available until {{$a}}.", "addon.mod_choice.numberofuser": "Number of responses", "addon.mod_choice.numberofuserinpercentage": "Percentage of responses", "addon.mod_choice.previewonly": "This is just a preview of the available options for this activity. You will not be able to submit your choice until {{$a}}.", @@ -602,6 +625,7 @@ "addon.mod_lti.modulenameplural": "External tools", "addon.mod_page.errorwhileloadingthepage": "Error while loading the page content.", "addon.mod_page.modulenameplural": "Pages", + "addon.mod_quiz.answercolon": "Answer:", "addon.mod_quiz.attemptfirst": "First attempt", "addon.mod_quiz.attemptlast": "Last attempt", "addon.mod_quiz.attemptnumber": "Attempt", @@ -732,6 +756,7 @@ "addon.mod_scorm.scormstatusnotdownloaded": "This SCORM package is not downloaded. It will be automatically downloaded when you open it.", "addon.mod_scorm.scormstatusoutdated": "This SCORM package has been modified since the last download. It will be automatically downloaded when you open it.", "addon.mod_scorm.suspended": "Suspended", + "addon.mod_scorm.toc": "TOC", "addon.mod_scorm.warningofflinedatadeleted": "Some offline data from attempt {{number}} has been discarded because it couldn't be counted as a new attempt.", "addon.mod_scorm.warningsynconlineincomplete": "Some attempts couldn't be synchronised with the site because the last online attempt is not yet finished. Please finish the online attempt first.", "addon.mod_survey.cannotsubmitsurvey": "Sorry, there was a problem submitting your survey. Please try again.", @@ -1246,6 +1271,7 @@ "core.courses.enrolme": "Enrol me", "core.courses.errorloadcategories": "An error occurred while loading categories.", "core.courses.errorloadcourses": "An error occurred while loading courses.", + "core.courses.errorloadplugins": "The plugins required by this course could not be loaded correctly. Please restart the app to try again.", "core.courses.errorsearching": "An error occurred while searching.", "core.courses.errorselfenrol": "An error occurred while self enrolling.", "core.courses.filtermycourses": "Filter my courses", @@ -1278,6 +1304,7 @@ "core.defaultvalue": "Default ({{$a}})", "core.delete": "Delete", "core.deletedoffline": "Deleted offline", + "core.deleteduser": "Deleted user", "core.deleting": "Deleting", "core.description": "Description", "core.dfdaymonthyear": "MM-DD-YYYY", @@ -1315,6 +1342,7 @@ "core.favourites": "Starred", "core.filename": "Filename", "core.filenameexist": "File name already exists: {{$a}}", + "core.filenotfound": "File not found, sorry.", "core.fileuploader.addfiletext": "Add file", "core.fileuploader.audio": "Audio", "core.fileuploader.camera": "Camera", @@ -1484,6 +1512,7 @@ "core.maxsizeandattachments": "Maximum size for new files: {{$a.size}}, maximum attachments: {{$a.attachments}}", "core.min": "min", "core.mins": "mins", + "core.misc": "Miscellaneous", "core.mod_assign": "Assignment", "core.mod_assignment": "Assignment 2.2 (Disabled)", "core.mod_book": "Book", @@ -1528,6 +1557,7 @@ "core.noresults": "No results", "core.notapplicable": "n/a", "core.notice": "Notice", + "core.notingroup": "Sorry, but you need to be part of a group to see this page.", "core.notsent": "Not sent", "core.now": "now", "core.numwords": "{{$a}} words", @@ -1567,11 +1597,20 @@ "core.question.questionno": "Question {{$a}}", "core.question.requiresgrading": "Requires grading", "core.quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.", + "core.rating.aggregateavg": "Average of ratings", + "core.rating.aggregatecount": "Count of ratings", + "core.rating.aggregatemax": "Maximum rating", + "core.rating.aggregatemin": "Minimum rating", + "core.rating.aggregatesum": "Sum of ratings", + "core.rating.noratings": "No ratings submitted", + "core.rating.rating": "Rating", + "core.rating.ratings": "Ratings", "core.redirectingtosite": "You will be redirected to the site.", "core.refresh": "Refresh", "core.remove": "Remove", "core.required": "Required", "core.requireduserdatamissing": "This user lacks some required profile data. Please enter the data in your site and try again.
{{$a}}", + "core.resourcedisplayopen": "Open", "core.resources": "Resources", "core.restore": "Restore", "core.retry": "Retry", @@ -1626,6 +1665,7 @@ "core.settings.navigatoruseragent": "Navigator userAgent", "core.settings.networkstatus": "Internet connection status", "core.settings.privacypolicy": "Privacy policy", + "core.settings.pushid": "Push notifications ID", "core.settings.reportinbackground": "Report errors automatically", "core.settings.settings": "Settings", "core.settings.showdownloadoptions": "Show download options", diff --git a/src/assets/lang/hi.json b/src/assets/lang/hi.json new file mode 100644 index 000000000..0870e0e6e --- /dev/null +++ b/src/assets/lang/hi.json @@ -0,0 +1,806 @@ +{ + "addon.badges.badgedetails": "पदक विवरण", + "addon.badges.badges": "पदक", + "addon.badges.contact": "संपर्क", + "addon.block_activitymodules.pluginname": "गतिविधियाँ", + "addon.block_myoverview.all": "सब", + "addon.block_myoverview.favourites": "तारांकित", + "addon.block_myoverview.future": "भविष्य", + "addon.block_myoverview.hiddencourses": "छिपा हुआ", + "addon.block_myoverview.inprogress": "चालू", + "addon.block_myoverview.lastaccessed": "अंतिम अक्सेस्सेड", + "addon.block_myoverview.morecourses": "अधिक पाठ्यक्रम", + "addon.block_myoverview.nocourses": "कोई पाठ्यक्रम नहीं", + "addon.block_myoverview.past": "अतीत", + "addon.block_myoverview.pluginname": "पाठ्यक्रम अवलोकन", + "addon.block_myoverview.title": "कोर्स का नाम", + "addon.block_recentlyaccessedcourses.nocourses": "कोई रीसेंट पाठ्यक्रम नहीं", + "addon.block_recentlyaccessedcourses.pluginname": "हाल ही में पहुँचा पाठ्यक्रम", + "addon.block_recentlyaccesseditems.noitems": "हाल की कोई आइटम नहीं", + "addon.block_recentlyaccesseditems.pluginname": "हाल ही में एक्सेस किए गए आइटम", + "addon.block_sitemainmenu.pluginname": "मुख्य मॅन्यु", + "addon.block_starredcourses.nocourses": "कोई तारांकित पाठ्यक्रम नहीं", + "addon.block_starredcourses.pluginname": "तारांकित पाठ्यक्रम", + "addon.block_timeline.duedate": "नियत तारीख", + "addon.block_timeline.next30days": "अगले 30 दिन", + "addon.block_timeline.next3months": "अगले 3 महीने", + "addon.block_timeline.next6months": "अगले 6 महीने", + "addon.block_timeline.next7days": "अगले 7 दिन", + "addon.block_timeline.nocoursesinprogress": "कोई इन-प्रोग्रेस कोर्सेस नहीं", + "addon.block_timeline.noevents": "कोई आगामी गतिविधियाँ नहीं बाकि", + "addon.block_timeline.overdue": "अतिदेय", + "addon.block_timeline.pluginname": "समय", + "addon.block_timeline.sortbycourses": "पाठ्यक्रमों द्वारा क्रमबद्ध करें", + "addon.block_timeline.sortbydates": "तिथियों के आधार पर छाँटें", + "addon.calendar.calendar": "कैलॅन्डर", + "addon.calendar.calendarevents": "कैलेंडर ईवेंट", + "addon.calendar.defaultnotificationtime": "डिफ़ॉल्ट सूचना समय", + "addon.calendar.errorloadevent": "ईवेंट लोड करने में त्रुटि।", + "addon.calendar.errorloadevents": "घटनाओं को लोड करने में त्रुटि।", + "addon.calendar.eventendtime": "समाप्ति समय", + "addon.calendar.eventstarttime": "आराम्भ समय", + "addon.calendar.noevents": "कोई घटना नहीं है", + "addon.competency.errornocompetenciesfound": "कोई योग्यता नहीं मिली", + "addon.competency.nocompetencies": "कोई योग्यता नहीं", + "addon.coursecompletion.complete": "पूर्ण", + "addon.coursecompletion.completecourse": "पूरा कोर्स", + "addon.coursecompletion.completed": "संपन्न", + "addon.coursecompletion.completiondate": "पूरा करने की तिथि", + "addon.coursecompletion.completionmenuitem": "समापन", + "addon.coursecompletion.couldnotloadreport": "पाठ्यक्रम पूरा होने की रिपोर्ट लोड नहीं कर सका। बाद में पुन: प्रयास करें।", + "addon.coursecompletion.coursecompletion": "पाठ्यक्रम सम्पूर्ण", + "addon.coursecompletion.criteria": "मानदंड", + "addon.coursecompletion.criteriagroup": "मानदंड समूह", + "addon.coursecompletion.criteriarequiredall": "नीचे दिए गए सभी मापदंडों आवश्यक हैं", + "addon.coursecompletion.criteriarequiredany": "नीचे दिए गए कोई भी मापदंड आवश्यक हैं", + "addon.coursecompletion.required": "ज़रूरी हैं", + "addon.coursecompletion.status": "दर्जा", + "addon.coursecompletion.viewcoursereport": "पाठ्यक्रम रिपोर्ट देखें", + "addon.files.couldnotloadfiles": "फाइलों की सूची लोड नहीं की जा सकी।", + "addon.files.emptyfilelist": "दिखाने के लिए फाइलें नहीं हैं।", + "addon.files.erroruploadnotworking": "दुर्भाग्य से वर्तमान में आपकी साइट पर फ़ाइलें अपलोड करना संभव नहीं है।", + "addon.files.files": "फ़ाइलें", + "addon.files.sitefiles": "साइट फ़ाइलें", + "addon.messageoutput_airnotifier.processorsettingsdesc": "उपकरणों को कॉन्फ़िगर करें", + "addon.messages.addcontact": "संपर्क जोड़ना", + "addon.messages.contactlistempty": "संपर्क सूची खाली है", + "addon.messages.contactname": "संपर्क नाम", + "addon.messages.contacts": "संपर्क", + "addon.messages.deletemessage": "संदेश को हटाएं", + "addon.messages.deletemessageconfirmation": "क्या आप निश्चित रूप से यह संदेश हटाना चाहते हैं? यह केवल आपके संदेश इतिहास से हटा दिया जाएगा और अभी भी उपयोगकर्ता द्वारा देखा जा सकता है जिसने संदेश भेजा या प्राप्त किया।", + "addon.messages.errordeletemessage": "संदेश हटाते समय त्रुटि।", + "addon.messages.errorwhileretrievingcontacts": "सर्वर से संपर्क पुनः प्राप्त करते समय त्रुटि।", + "addon.messages.errorwhileretrievingdiscussions": "सर्वर से चर्चाएँ प्राप्त करते समय त्रुटि।", + "addon.messages.errorwhileretrievingmessages": "सर्वर से संदेश प्राप्त करते समय त्रुटि।", + "addon.messages.errorwhileretrievingusers": "सर्वर से उपयोगकर्ताओं को पुनर्प्राप्त करते समय त्रुटि।", + "addon.messages.message": "सन्देश", + "addon.messages.messagenotsent": "संदेश नहीं भेजा गया था। बाद में पुन: प्रयास करें।", + "addon.messages.messages": "सन्देश", + "addon.messages.newmessages": "नए संदेश", + "addon.messages.nousersfound": "कोई उपयोग्कर्ता नहीं मिले", + "addon.messages.showdeletemessages": "डिलीट मैसेज दिखाओ", + "addon.messages.type_blocked": "अवरोधित", + "addon.messages.type_offline": "ऑफलाइन", + "addon.messages.type_online": "ऑनलाइन", + "addon.messages.type_search": "खोज परिणाम", + "addon.messages.type_strangers": "अन्य लोग", + "addon.messages.warningconversationmessagenotsent": "बातचीत के लिए संदेश नहीं भेजा जा सका {{conversation}}. {{error}}", + "addon.messages.warningmessagenotsent": "उपयोगकर्ता को संदेश (संदेश) नहीं भेज सका{{user}}. {{error}}", + "addon.mod_assign.acceptsubmissionstatement": "कृपया सबमिशन स्टेटमेंट स्वीकार करें।", + "addon.mod_assign.addattempt": "एक और प्रयास को अनुमति दें", + "addon.mod_assign.addnewattempt": "एक नया प्रयास जोड़ें", + "addon.mod_assign.addsubmission": "प्रस्तुति जोड़ें", + "addon.mod_assign.allowsubmissionsfromdate": "से प्रस्तुतियाँ की अनुमति", + "addon.mod_assign.assignmentisdue": "कार्य उपयुक्त है", + "addon.mod_assign.attemptreopenmethod_manual": "हाथ से", + "addon.mod_assign.cannoteditduetostatementsubmission": "आप एप्लिकेशन में कोई सबमिशन जोड़ या संपादित नहीं कर सकते क्योंकि सबमिशन स्टेटमेंट को साइट से पुनर्प्राप्त नहीं किया जा सकता था", + "addon.mod_assign.cannotgradefromapp": "कुछ ग्रेडिंग विधियों को अभी तक ऐप द्वारा समर्थित नहीं किया गया है और उन्हें संशोधित नहीं किया जा सकता है।", + "addon.mod_assign.cannotsubmitduetostatementsubmission": "आप एप्लिकेशन में एक सबमिशन नहीं कर सकते क्योंकि सबमिशन स्टेटमेंट को साइट से पुनर्प्राप्त नहीं किया जा सकता था।", + "addon.mod_assign.erroreditpluginsnotsupported": "आप एप्लिकेशन में कोई सबमिशन जोड़ या संपादित नहीं कर सकते क्योंकि कुछ प्लगइन्स अभी तक संपादन के लिए समर्थित नहीं हैं।", + "addon.mod_assign.errorshowinginformation": "सबमिशन की जानकारी प्रदर्शित नहीं की जा सकती।", + "addon.mod_assign.feedbacknotsupported": "यह फ़ीडबैक ऐप द्वारा समर्थित नहीं है और इसमें सभी जानकारी नहीं हो सकती है।", + "addon.mod_assign.gradenotsynced": "ग्रेड सिंक नहीं किया गया", + "addon.mod_assign.notallparticipantsareshown": "जिन प्रतिभागियों ने प्रस्तुत नहीं किया है, उन्हें नहीं दिखाया गया है।", + "addon.mod_assign.numwords": "{{$a}} शब्द", + "addon.mod_assign.submissionnotsupported": "यह सबमिशन ऐप द्वारा समर्थित नहीं है और इसमें सभी जानकारी नहीं हो सकती है।", + "addon.mod_assign.userwithid": "आईडी के साथ उपयोगकर्ता {{id}}", + "addon.mod_assign.warningsubmissiongrademodified": "प्रस्तुत ग्रेड को साइट पर संशोधित किया गया था।", + "addon.mod_assign.warningsubmissionmodified": "उपयोगकर्ता प्रस्तुत साइट पर संशोधित किया गया था।", + "addon.mod_assign_feedback_comments.pluginname": "प्रतिक्रिया टिप्पणी", + "addon.mod_assign_feedback_editpdf.pluginname": "एनोटेट पीडीएफ", + "addon.mod_assign_feedback_file.pluginname": "फ़ाइल प्रतिक्रिया", + "addon.mod_assign_submission_comments.pluginname": "प्रस्तुत टिप्पणी", + "addon.mod_assign_submission_file.pluginname": "फ़ाइल प्रस्तुतियाँ", + "addon.mod_chat.beep": "बीप्", + "addon.mod_chat.chatreport": "चैट सेशन", + "addon.mod_chat.currentusers": "वर्तमान यूज़र", + "addon.mod_chat.errorwhileconnecting": "चैट से कनेक्ट करते समय त्रुटि।", + "addon.mod_chat.errorwhilegettingchatdata": "चैट डेटा प्राप्त करते समय त्रुटि।", + "addon.mod_chat.errorwhilegettingchatusers": "चैट उपयोगकर्ताओं को प्राप्त करते समय त्रुटि।", + "addon.mod_chat.errorwhileretrievingmessages": "सर्वर से संदेश प्राप्त करते समय त्रुटि।", + "addon.mod_chat.errorwhilesendingmessage": "संदेश भेजते समय त्रुटि।", + "addon.mod_chat.messages": "सन्देश", + "addon.mod_chat.modulenameplural": "चैट्स्", + "addon.mod_chat.mustbeonlinetosendmessages": "आपको संदेश भेजने के लिए ऑनलाइन होना चाहिए।", + "addon.mod_chat.nomessages": "कोई सन्देश नहीं", + "addon.mod_choice.errorgetchoice": "विकल्प डेटा प्राप्त करने में त्रुटि।", + "addon.mod_choice.responsesresultgraphdescription": "{{number}}% उपयोगकर्ताओं ने विकल्प चुना: {{text}}.", + "addon.mod_choice.resultsnotsynced": "परिणामों में शामिल करने से पहले आपकी अंतिम प्रतिक्रिया को सिंक्रनाइज़ किया जाना चाहिए।", + "addon.mod_data.errorapproving": "प्रविष्टि को अस्वीकार या अस्वीकार करने में त्रुटि।", + "addon.mod_data.errordeleting": "प्रविष्टि हटाने में त्रुटि।", + "addon.mod_feedback.captchaofflinewarning": "कैप्चा के साथ प्रतिक्रिया ऑफ़लाइन पूरी नहीं हो सकती है, या यदि कॉन्फ़िगर नहीं है, या यदि सर्वर डाउन है।", + "addon.mod_feedback.feedback_submitted_offline": "इस फ़ीडबैक को बाद में सबमिट करने के लिए सहेजा गया है।", + "addon.mod_folder.emptyfilelist": "दिखाने के लिए फाइलें नहीं हैं।", + "addon.mod_forum.addanewtopic": "नया विषय जोड़िये", + "addon.mod_forum.discussion": "चर्चा", + "addon.mod_forum.errorgetforum": "फ़ोरम डेटा प्राप्त करने में त्रुटि।", + "addon.mod_forum.errorgetgroups": "समूह सेटिंग्स प्राप्त करने में त्रुटि।", + "addon.mod_forum.forumnodiscussionsyet": "इस फोरम में अभी कोई चर्चा नहीं हुई है।", + "addon.mod_forum.group": "समूह", + "addon.mod_forum.message": "सन्देश", + "addon.mod_forum.modulenameplural": "फ़ोरम", + "addon.mod_forum.numdiscussions": "{{numdiscussions}} चर्चाएँ", + "addon.mod_forum.numreplies": "{{numreplies}} उत्तर", + "addon.mod_forum.refreshdiscussions": "चर्चा को ताज़ा करें", + "addon.mod_forum.refreshposts": "पदों को ताज़ा करें", + "addon.mod_forum.reply": "उत्तर दीजिए", + "addon.mod_forum.subject": "विषय", + "addon.mod_glossary.browsemode": "प्रविष्टियां ब्राउज़ करें", + "addon.mod_glossary.byalphabet": "वर्णक्रम", + "addon.mod_glossary.byauthor": "लेखक द्वारा समूह", + "addon.mod_glossary.bycategory": "श्रेणी के अनुसार समूह", + "addon.mod_glossary.bynewestfirst": "नवीनतम पहले", + "addon.mod_glossary.byrecentlyupdated": "हाल ही में अद्यतित", + "addon.mod_glossary.bysearch": "खोज", + "addon.mod_glossary.cannoteditentry": "प्रविष्टि संपादित नहीं की जा सकती", + "addon.mod_glossary.definition": "परिभाषा", + "addon.mod_glossary.entriestobesynced": "प्रविष्ट किए जाने की प्रविष्टियाँ", + "addon.mod_glossary.entrypendingapproval": "यह प्रविष्टि लंबित अनुमोदन है।", + "addon.mod_glossary.errorloadingentries": "प्रविष्टियाँ लोड करते समय एक त्रुटि हुई।", + "addon.mod_glossary.errorloadingentry": "प्रविष्टि लोड करते समय एक त्रुटि हुई।", + "addon.mod_glossary.errorloadingglossary": "शब्दकोष लोड करते समय एक त्रुटि हुई।", + "addon.mod_glossary.noentriesfound": "कोई प्रविष्टि नहीं मिली।", + "addon.mod_glossary.searchquery": "पूछताछ कीजिए", + "addon.mod_imscp.showmoduledescription": "विवरण दिखाएं", + "addon.mod_lesson.answer": "उत्तर", + "addon.mod_lesson.errorprefetchrandombranch": "इस पाठ में एक रैंडम कंटेंट पेज पर जंप होता है। जब तक इसे वेब ब्राउज़र में शुरू नहीं किया गया है, तब तक इसे ऐप में डालने का प्रयास नहीं किया जा सकता है।", + "addon.mod_lesson.errorreviewretakenotlast": "इस प्रयास की अब समीक्षा नहीं की जा सकती है क्योंकि एक और प्रयास समाप्त हो चुका है।", + "addon.mod_lesson.finishretakeoffline": "यह प्रयास ऑफ़लाइन समाप्त हो गया था।", + "addon.mod_lesson.or": "या", + "addon.mod_lesson.question": "प्रश्न", + "addon.mod_lesson.retakefinishedinsync": "एक ऑफ़लाइन प्रयास सिंक्रनाइज़ किया गया था। क्या आप इसकी समीक्षा करना चाहते हैं?", + "addon.mod_lesson.retakelabelfull": "{{retake}}: {{grade}} {{timestart}} ({{duration}})", + "addon.mod_lesson.retakelabelshort": "{{retake}}: {{grade}} {{timestart}}", + "addon.mod_lesson.warningretakefinished": "साइट पर प्रयास समाप्त हो गया था।", + "addon.mod_lti.errorgetlti": "मॉड्यूल डेटा प्राप्त करने में त्रुटि।", + "addon.mod_lti.errorinvalidlaunchurl": "लॉन्च URL मान्य नहीं है।", + "addon.mod_lti.launchactivity": "गतिविधि का शुभारंभ करें", + "addon.mod_page.errorwhileloadingthepage": "पृष्ठ सामग्री लोड करते समय त्रुटि।", + "addon.mod_quiz.cannotsubmitquizdueto": "यह प्रश्नोत्तरी प्रयास निम्नलिखित कारणों से प्रस्तुत नहीं किया जा सकता है:", + "addon.mod_quiz.confirmcontinueoffline": "यह प्रयास {{$a}} के बाद से सिंक्रनाइज़ नहीं किया गया है। यदि आपने इस प्रयास को किसी अन्य डिवाइस में जारी रखा है, तो आप डेटा खो सकते हैं।", + "addon.mod_quiz.confirmleavequizonerror": "उत्तर सहेजते समय एक त्रुटि हुई। क्या आप वाकई क्विज़ छोड़ना चाहते हैं?", + "addon.mod_quiz.errorbehaviournotsupported": "इस क्विज़ को ऐप में डालने का प्रयास नहीं किया जा सकता क्योंकि प्रश्न व्यवहार ऐप द्वारा समर्थित नहीं है:", + "addon.mod_quiz.errordownloading": "आवश्यक डेटा डाउनलोड करने में त्रुटि।", + "addon.mod_quiz.errorgetattempt": "प्रयास डेटा प्राप्त करने में त्रुटि।", + "addon.mod_quiz.errorgetquestions": "प्रश्न प्राप्त करने में त्रुटि।", + "addon.mod_quiz.errorgetquiz": "प्रश्नोत्तरी डेटा प्राप्त करने में त्रुटि", + "addon.mod_quiz.errorparsequestions": "प्रश्न पढ़ते समय एक त्रुटि हुई। कृपया वेब ब्राउज़र में इस क्विज़ का प्रयास करें।", + "addon.mod_quiz.errorquestionsnotsupported": "इस क्विज़ को ऐप में लेने का प्रयास नहीं किया जा सकता है क्योंकि इसमें ऐप द्वारा समर्थित प्रश्न नहीं हैं:", + "addon.mod_quiz.errorrulesnotsupported": "इस क्विज़ को ऐप में लेने का प्रयास नहीं किया जा सकता क्योंकि इसमें ऐप द्वारा समर्थित नियम नहीं हैं:", + "addon.mod_quiz.errorsaveattempt": "प्रयास डेटा सहेजते समय एक त्रुटि हुई।", + "addon.mod_quiz.finishnotsynced": "समाप्त हो गया, लेकिन सिंक्रनाइज़ नहीं किया गया", + "addon.mod_quiz.opentoc": "नेविगेशन पॉपओवर खोलें", + "addon.mod_quiz.warningattemptfinished": "ऑफ़लाइन प्रयास खारिज कर दिया गया क्योंकि यह साइट पर समाप्त हो गया था या नहीं मिला।", + "addon.mod_quiz.warningdatadiscarded": "कुछ ऑफ़लाइन उत्तरों को छोड़ दिया गया क्योंकि प्रश्न ऑनलाइन संशोधित किए गए थे।", + "addon.mod_quiz.warningdatadiscardedfromfinished": "कुछ अधूरे उत्तर छोड़ दिए जाने के कारण अधूरा रह गया। कृपया अपने उत्तरों की समीक्षा करें और फिर प्रयास को फिर से शुरू करें।", + "addon.mod_resource.errorwhileloadingthecontent": "सामग्री लोड करते समय त्रुटि।", + "addon.mod_resource.openthefile": "फ़ाइल खोलें", + "addon.mod_scorm.cannotcalculategrade": "ग्रेड की गणना नहीं की जा सकी।", + "addon.mod_scorm.dataattemptshown": "यह डेटा प्रयास नंबर {{number}} का है।", + "addon.mod_scorm.errorcreateofflineattempt": "नया ऑफ़लाइन प्रयास करते समय एक त्रुटि हुई। कृपया पुन: प्रयास करें", + "addon.mod_scorm.errordownloadscorm": "SCORM डाउनलोड करने में त्रुटि:\"{{name}}\".", + "addon.mod_scorm.errorgetscorm": "SCORM डेटा प्राप्त करने में त्रुटि।", + "addon.mod_scorm.errorinvalidversion": "क्षमा करें, एप्लिकेशन केवल SCORM 1.2 का समर्थन करता है।", + "addon.mod_scorm.errornotdownloadable": "SCORM पैकेज का डाउनलोड अक्षम है। कृपया अपने साइट व्यवस्थापक से संपर्क करें।", + "addon.mod_scorm.errornovalidsco": "इस SCORM पैकेज में लोड करने के लिए एक दृश्य SCO नहीं है।", + "addon.mod_scorm.errorpackagefile": "क्षमा करें, एप्लिकेशन केवल ज़िप पैकेज का समर्थन करता है।", + "addon.mod_scorm.errorsyncscorm": "सिंक्रनाइज़ करते समय एक त्रुटि हुई। कृपया पुन: प्रयास करें।", + "addon.mod_scorm.modulenameplural": "SCORMs", + "addon.mod_scorm.offlineattemptnote": "इस प्रयास में ऐसा डेटा है जिसे सिंक्रनाइज़ नहीं किया गया है।", + "addon.mod_scorm.offlineattemptovermax": "यह प्रयास नहीं भेजा जा सकता है क्योंकि आप अधिकतम प्रयासों को पार कर चुके हैं।", + "addon.mod_scorm.passed": "उत्तीर्ण", + "addon.mod_scorm.scormstatusnotdownloaded": "यह SCORM पैकेज डाउनलोड नहीं किया गया है। इसे खोलते ही यह अपने आप डाउनलोड हो जाएगा।", + "addon.mod_scorm.scormstatusoutdated": "इस SCORM पैकेज को अंतिम डाउनलोड के बाद संशोधित किया गया है। इसे खोलते ही यह अपने आप डाउनलोड हो जाएगा।", + "addon.mod_scorm.warningofflinedatadeleted": "प्रयास {{number}} से कुछ ऑफ़लाइन डेटा को छोड़ दिया गया है क्योंकि इसे नए प्रयास के रूप में नहीं गिना जा सकता।", + "addon.mod_scorm.warningsynconlineincomplete": "कुछ प्रयास साइट के साथ सिंक्रनाइज़ नहीं किए जा सके क्योंकि अंतिम ऑनलाइन प्रयास अभी तक समाप्त नहीं हुआ है। कृपया पहले ऑनलाइन प्रयास समाप्त करें।", + "addon.mod_survey.cannotsubmitsurvey": "क्षमा करें, आपके सर्वेक्षण को सबमिट करने में एक समस्या थी। कृपया पुन: प्रयास करें।", + "addon.mod_survey.errorgetsurvey": "सर्वेक्षण डेटा प्राप्त करने में त्रुटि।", + "addon.mod_survey.results": "परिणाम", + "addon.mod_url.accessurl": "URL तक पहुँचें", + "addon.mod_url.pointingtourl": "URL जो संसाधन को इंगित करता है।", + "addon.mod_wiki.errorloadingpage": "पृष्ठ लोड करते समय एक त्रुटि हुई।", + "addon.mod_wiki.errornowikiavailable": "इस विकी के पास अभी तक कोई सामग्री नहीं है।", + "addon.mod_wiki.gowikihome": "इस विकी के पास अभी तक कोई सामग्री नहीं है।", + "addon.mod_wiki.subwiki": "उप-विकि", + "addon.mod_wiki.titleshouldnotbeempty": "शीर्षक खाली नहीं होना चाहिए", + "addon.mod_wiki.viewpage": "पृष्ठ देखें", + "addon.mod_wiki.wikipage": "विकी पेज", + "addon.mod_workshop.assessmentstrategynotsupported": "मूल्यांकन रणनीति {{$a}} समर्थित नहीं है", + "addon.mod_workshop.submissionrequiredtitle": "You need to enter a title.", + "addon.mod_workshop.warningassessmentmodified": "साइट पर सबमिशन संशोधित किया गया था।", + "addon.mod_workshop.warningsubmissionmodified": "मूल्यांकन साइट पर संशोधित किया गया था।", + "addon.notes.userwithid": "आईडी {{id}} के साथ उपयोगकर्ता", + "addon.notes.warningnotenotsent": "नोट (ओं) को निश्चित रूप से नहीं जोड़ा जा सका {{course}}. {{error}}", + "addon.notifications.errorgetnotifications": "सूचनाएं प्राप्त करने में त्रुटि।", + "addon.notifications.notifications": "सूचनाएं", + "addon.notifications.playsound": "ध्वनि खेलने", + "addon.notifications.therearentnotificationsyet": "कोई सूचना नहीं है।", + "assets.countries.AD": "एंडोरा", + "assets.countries.AE": "संयुक्त अरब Emirates", + "assets.countries.AF": "अफ़्गानिस्तान", + "assets.countries.AG": "एंटीगुआ और बारबुडा", + "assets.countries.AL": "अल्बानिया", + "assets.countries.AM": "अर्मेनिआ", + "assets.countries.AO": "अंगोला", + "assets.countries.AQ": "अन्टार्टिका", + "assets.countries.AR": "अर्जेंटीना", + "assets.countries.AS": "अमेरिकन समोआ", + "assets.countries.AT": "ऑस्ट्रिया", + "assets.countries.AU": "ऑस्ट्रेलिया", + "assets.countries.AW": "अरूबा", + "assets.countries.BB": "बार्बाडोस", + "assets.countries.BD": "बांग्लादेश", + "assets.countries.BE": "बेल्जियम", + "assets.countries.BG": "बुल्गारिया", + "assets.countries.BH": "बहरीन", + "assets.countries.BJ": "बेनिन", + "assets.countries.BM": "बर्मूडा", + "assets.countries.BN": "ब्रुनेइ दारुस्सलम", + "assets.countries.BR": "ब्राज़ील", + "assets.countries.BS": "बहामाज़", + "assets.countries.BT": "भूटान", + "assets.countries.BW": "बोट्स्वाना", + "assets.countries.BY": "बेलारूस", + "assets.countries.BZ": "बेलीज़", + "assets.countries.CA": "कनाडा", + "assets.countries.CF": "मध्य अफ्रीकी गणराज्य", + "assets.countries.CG": "कौंगो", + "assets.countries.CL": "चिली", + "assets.countries.CM": "कैमेरून", + "assets.countries.CN": "चीन", + "assets.countries.CU": "क्यूबा", + "assets.countries.CY": "साय्प्रस", + "assets.countries.CZ": "चेक गणराज्य", + "assets.countries.DE": "जर्मनी", + "assets.countries.DK": "डेन्मार्क", + "assets.countries.EH": "पश्चिमी सहारा", + "assets.countries.ES": "स्पेन", + "assets.countries.ET": "इथियोपिया", + "assets.countries.FI": "फ़िनलैंड", + "assets.countries.FJ": "फ़िजी", + "assets.countries.FR": "फ़्रांस", + "assets.countries.GB": "यूनाइटेड किंगडम", + "assets.countries.GE": "जॉर्जिया", + "assets.countries.GH": "घाना", + "assets.countries.GL": "ग्रीन्लैन्ड", + "assets.countries.GR": "ग्रीस", + "assets.countries.GT": "ग्वाटेमाला", + "assets.countries.GY": "गुयाना", + "assets.countries.HK": "हांग कांग", + "assets.countries.HN": "होंडुरस", + "assets.countries.HR": "क्रोएशिया", + "assets.countries.HT": "हैती", + "assets.countries.HU": "हंगरी", + "assets.countries.ID": "इंडोनेशिया", + "assets.countries.IE": "आयरलैंड", + "assets.countries.IL": "इज़रायल", + "assets.countries.IN": "भारत", + "assets.countries.IQ": "इराक", + "assets.countries.IT": "इटली", + "assets.countries.JP": "जापान", + "assets.countries.KZ": "कजाखस्तान", + "assets.countries.LB": "लेबनान", + "assets.countries.LI": "लिचेंस्टीन", + "assets.countries.LK": "श्रीलंका", + "assets.countries.LR": "लाइबेरिया", + "assets.countries.LT": "लिथुआनिया", + "assets.countries.LU": "लक्ज़मबर्ग", + "assets.countries.LY": "लीबिया", + "assets.countries.MA": "मोरक्को", + "assets.countries.MC": "मोनाको", + "assets.countries.MG": "मेडागास्कर", + "assets.countries.ML": "माली", + "assets.countries.MM": "म्यांमार", + "assets.countries.MN": "मंगोलिया", + "assets.countries.MU": "मॉरिशस", + "assets.countries.MV": "मालद्वीप", + "assets.countries.MX": "मेक्सिको", + "assets.countries.MY": "मलेशिया", + "assets.countries.PH": "फ़िलिपींस", + "assets.countries.PK": "पाकिस्तान", + "assets.countries.PL": "पोलैंड", + "assets.countries.PT": "पुर्तगाल", + "assets.countries.QA": "क़तर", + "assets.countries.RO": "रोमानिया", + "assets.countries.RS": "सर्बिआ", + "assets.countries.SA": "सऊदी अरब", + "assets.countries.SD": "सूडान", + "assets.countries.SE": "स्वीडन", + "assets.countries.SG": "सिंगापुर", + "assets.countries.SI": "स्लोवेनिया", + "assets.countries.SK": "स्लोवाकिया", + "assets.countries.SN": "सेनेगल", + "assets.countries.SO": "सोमालिया", + "assets.countries.TR": "टर्की", + "assets.countries.TT": "त्रिनिदाद एवं टोबेगो", + "assets.countries.TW": "ताइवान", + "assets.countries.UA": "यूक्रेन", + "assets.countries.UG": "यूगांडा", + "assets.countries.US": "अमेरिका", + "assets.countries.UY": "उरुग्वे", + "assets.countries.UZ": "उज़्बेकिस्तान", + "assets.countries.VN": "वियतनाम", + "assets.countries.YE": "यमन", + "assets.countries.ZA": "दक्षिण अफ्रीका", + "assets.countries.ZW": "ज़िम्बाब्वे", + "core.accounts": "खाते ", + "core.add": "जोड़िए", + "core.agelocationverification": "आयु और स्थान सत्यापन", + "core.ago": "{{$a}} पहले", + "core.all": "सभी", + "core.allparticipants": "सभी प्रतिभागी", + "core.android": "एंड्रॉयड", + "core.answer": "जवाब", + "core.areyousure": "आप को यकीन हैं ?", + "core.back": "वापस", + "core.cancel": "कैन्सॅल", + "core.cannotconnect": "कनेक्ट नहीं कर सकता: सत्यापित करें कि आपने URL को सही प्रकार से टाइप किया है और आपकी साइट Moodle 2.4 या बाद का उपयोग करती है।", + "core.cannotdownloadfiles": "फ़ाइल डाउनलोडिंग अक्षम है। कृपया अपने साइट व्यवस्थापक से संपर्क करें।", + "core.captureaudio": "ध्वनि रिकॉर्ड करें", + "core.capturedimage": "चित्र लिया।", + "core.captureimage": "तस्वीर ले लो", + "core.capturevideo": "वीडियो रिकॉर्ड करो", + "core.category": "वर्ग", + "core.choose": "चुनिए", + "core.choosedots": "चुनें ...", + "core.clearsearch": "स्पष्ट खोज", + "core.clicktohideshow": "विस्तार या पतन के लिए क्लिक करें", + "core.clicktoseefull": "पूर्ण सामग्री देखने के लिए क्लिक करें।", + "core.comments": "टिप्पणियां", + "core.commentscount": "टिप्पणियाँ ({{$a}})", + "core.commentsnotworking": "टिप्पणियों को पुनर्प्राप्त नहीं किया जा सकता है", + "core.completion-alt-auto-fail": "समाप्त किया: {{$a}} (पास ग्रेड प्राप्त नहीं हुआ)", + "core.completion-alt-auto-n": "समाप्त नहीं हुई: {{$a}}", + "core.completion-alt-auto-n-override": "समाप्त नहीं हुई: {{$a.modname}} (set by {{$a.overrideuser}})", + "core.completion-alt-auto-pass": "समाप्त: {{$a}} (पास ग्रेड हासिल किया गया)", + "core.completion-alt-auto-y": "समाप्त: {{$a}}", + "core.completion-alt-auto-y-override": "समाप्त: {{$a.modname}} ({{$a.overrideuser}} द्वारा निर्धारित)", + "core.completion-alt-manual-n": "समाप्त नहीं हुआ: {{$a}} समाप्त के रूप में चिह्नित करने के लिए चुनें।", + "core.completion-alt-manual-n-override": "समाप्त नहीं हुआ: {{$a.modname}} ({{$a.overrideuser}} द्वारा निर्धारित) समाप्त के रूप में चिह्नित करने के लिए चुनें।", + "core.completion-alt-manual-y": "समाप्त हो गया: {{$a}} समाप्त नहीं होने के रूप में चिह्नित करने के लिए चुनें।", + "core.completion-alt-manual-y-override": "समाप्त हो गया: {{$a.modname}} ({{$a.overrideuser}} द्वारा निर्धारित) समाप्त नहीं होने के रूप में चिह्नित करने के लिए चुनें।", + "core.confirmcanceledit": "क्या आप वाकई इस पृष्ठ को छोड़ना चाहते हैं? सभी परिवर्तन खो जाएंगे।", + "core.confirmloss": "क्या आपको यकीन है? सभी परिवर्तन खो जाएंगे।", + "core.confirmopeninbrowser": "क्या आप इसे वेब ब्राउज़र में खोलना चाहते हैं?", + "core.considereddigitalminor": "इस साइट पर खाता बनाने के लिए आप बहुत छोटे हैं।", + "core.content": "सामग्री", + "core.contenteditingsynced": "आपके द्वारा संपादित की जा रही सामग्री सिंक हो गई है।", + "core.contentlinks.chooseaccount": "खाता चुनें", + "core.contentlinks.chooseaccounttoopenlink": "लिंक खोलने के लिए एक खाता चुनें।", + "core.contentlinks.confirmurlothersite": "यह लिंक दूसरी साइट का है। क्या आप इसे खोलना चाहते हैं?", + "core.contentlinks.errornoactions": "इस लिंक के साथ प्रदर्शन करने के लिए कोई क्रिया नहीं मिली।", + "core.contentlinks.errornosites": "इस लिंक को संभालने के लिए कोई साइट नहीं मिली।", + "core.continue": "जारी रखें", + "core.copiedtoclipboard": "क्लिपबोर्ड पर पाठ कॉपी किया गया", + "core.course": "कोर्स", + "core.course.activitydisabled": "आपके संगठन ने इस गतिविधि को मोबाइल ऐप में अक्षम कर दिया है।", + "core.course.activitynotyetviewableremoteaddon": "आपके संगठन ने एक प्लगइन स्थापित किया है जो अभी तक समर्थित नहीं है।", + "core.course.activitynotyetviewablesiteupgradeneeded": "आपके संगठन की Moodle स्थापना को अद्यतन करने की आवश्यकता है।", + "core.course.allsections": "सभी वर्गों", + "core.course.askadmintosupport": "साइट व्यवस्थापक से संपर्क करें और उन्हें बताएं कि आप इस गतिविधि का उपयोग Moodle मोबाइल ऐप के साथ करना चाहते हैं।", + "core.course.confirmdeletemodulefiles": "क्या आप वाकई इन फ़ाइलों को हटाना चाहते हैं?", + "core.course.confirmdownload": "आप {{size}} डाउनलोड करने वाले हैं। क्या तुम वाकई जारी रखना चाहते हो?", + "core.course.confirmdownloadunknownsize": "डाउनलोड के आकार की गणना करना संभव नहीं था। क्या तुम वाकई जारी रखना चाहते हो?", + "core.course.confirmpartialdownloadsize": "आप कम से कम {{size}} डाउनलोड करने वाले हैं। क्या तुम वाकई जारी रखना चाहते हो?", + "core.course.contents": "अंतर्वस्तु", + "core.course.couldnotloadsectioncontent": "अनुभाग सामग्री लोड नहीं की जा सकी। बाद में पुन: प्रयास करें।", + "core.course.couldnotloadsections": "अनुभागों को लोड नहीं किया जा सका। बाद में पुन: प्रयास करें।", + "core.course.coursesummary": "पाठ्यक्रम सारांश", + "core.course.errordownloadingcourse": "पाठ्यक्रम डाउनलोड करने में त्रुटि।", + "core.course.errordownloadingsection": "अनुभाग डाउनलोड करने में त्रुटि।", + "core.course.errorgetmodule": "गतिविधि डेटा प्राप्त करने में त्रुटि।", + "core.course.hiddenfromstudents": "छात्रों से छिपा हुआ", + "core.course.hiddenoncoursepage": "उपलब्ध है लेकिन पाठ्यक्रम पृष्ठ पर नहीं दिखाया गया है", + "core.course.manualcompletionnotsynced": "मैनुअल पूरा नहीं सिंक्रनाइज़ किया गया", + "core.course.nocontentavailable": "फिलहाल कोई सामग्री उपलब्ध नहीं है।", + "core.course.refreshcourse": "रिफ्रेश कोर्स", + "core.course.sections": "अनुभागें", + "core.course.useactivityonbrowser": "आप अभी भी अपने डिवाइस के वेब ब्राउज़र का उपयोग करके इसका उपयोग कर सकते हैं।", + "core.course.warningmanualcompletionmodified": "एक गतिविधि के मैनुअल पूरा होने को साइट पर संशोधित किया गया था।", + "core.course.warningofflinemanualcompletiondeleted": "निश्चित रूप से '{{name}}' के कुछ ऑफ़लाइन मैनुअल को हटा दिया गया है। {{error}}", + "core.coursedetails": "पाठ्यक्रम विवरण", + "core.courses.addtofavourites": "इस पाठ्यक्रम को स्टार दें", + "core.courses.allowguests": "यह कोर्स अतिथि यूज़र्स को प्रवेश करने देता है", + "core.courses.availablecourses": "उपलब्ध पाठ्यक्रम", + "core.courses.cannotretrievemorecategories": "स्तर {{$ a}} से अधिक गहरी श्रेणियों को पुनर्प्राप्त नहीं किया जा सकता है।", + "core.courses.categories": "कोर्स वर्ग", + "core.courses.confirmselfenrol": "क्या आप वाकई इस पाठ्यक्रम में स्वयं को नामांकित करना चाहते हैं?", + "core.courses.courses": "पाठ्यक्रम", + "core.courses.downloadcourses": "पाठ्यक्रम डाउनलोड करें", + "core.courses.enrolme": "मेरे दाखिला लिया", + "core.courses.errorloadcategories": "श्रेणियां लोड करते समय एक त्रुटि हुई।", + "core.courses.errorloadcourses": "पाठ्यक्रम लोड करते समय एक त्रुटि हुई", + "core.courses.errorsearching": "खोज करते समय एक त्रुटि हुई।", + "core.courses.errorselfenrol": "स्वयं नामांकन करते समय एक त्रुटि हुई।", + "core.courses.filtermycourses": "मेरे पाठ्यक्रमों को फ़िल्टर करें", + "core.courses.hidecourse": "व्यू से छिपाना", + "core.courses.mycourses": "मेरे कोर्सस", + "core.courses.nocoursesyet": "इस वर्ग में कोई कोर्स उपलब्ध नहीं है", + "core.courses.notenrollable": "आप इस पाठ्यक्रम में अपना नामांकन नहीं कर सकते।", + "core.courses.password": "नामांकन की कुंजी", + "core.courses.removefromfavourites": "इस कोर्स को अनस्टार करें", + "core.courses.search": "खोज", + "core.courses.searchcourses": "कोर्स की खोज करें", + "core.courses.searchcoursesadvice": "आप अतिथि के रूप में उपयोग करने के लिए पाठ्यक्रम खोजने के लिए खोज पाठ्यक्रम बटन का उपयोग कर सकते हैं या उन पाठ्यक्रमों में खुद को नामांकित कर सकते हैं जो इसे अनुमति देते हैं।", + "core.courses.selfenrolment": "स्व नामांकन करना", + "core.courses.show": "इस कोर्स को दिखाएं", + "core.courses.totalcoursesearchresults": "कुल पाठ्यक्रम: {{$a}}", + "core.currentdevice": "वर्तमान डिवाइस", + "core.datastoredoffline": "डिवाइस में डेटा संग्रहीत है क्योंकि इसे भेजा नहीं जा सका है। इसे बाद में स्वचालित रूप से भेजा जाएगा।", + "core.date": "तारीख", + "core.day": "दिन", + "core.days": "दिन", + "core.decsep": ".", + "core.delete": "डिलीट", + "core.deletedoffline": "ऑफ़लाइन हटा दिया गया", + "core.deleting": "हटाया जा रहा है", + "core.description": "विवरण", + "core.dfdaymonthyear": "MM-DD-YYYY", + "core.dfdayweekmonth": "ddd, D MMM", + "core.dffulldate": "dddd, D MMMM YYYY h[:]mm A", + "core.dflastweekdate": "ddd", + "core.dfmediumdate": "LLL", + "core.dftimedate": "h[:]mm A", + "core.digitalminor": "डिजिटल नाबालिग", + "core.digitalminor_desc": "इस साइट पर खाता बनाने के लिए कृपया अपने माता-पिता / अभिभावक को निम्नलिखित व्यक्ति से संपर्क करें।", + "core.discard": "छोड़ना", + "core.dismiss": "खारिज", + "core.download": "डाउनलोड", + "core.downloading": "डाउनलोड कर रहा है", + "core.edit": " का सम्पादन कीजिए", + "core.emptysplit": "यदि बाएं पैनल खाली है या लोड हो रहा है तो यह पृष्ठ रिक्त दिखाई देगा", + "core.error": "गलती", + "core.errorchangecompletion": "पूर्ण स्थिति बदलते समय एक त्रुटि हुई। कृपया पुन: प्रयास करें।", + "core.errordeletefile": "फ़ाइल को हटाने में त्रुटि। कृपया पुन: प्रयास करें।", + "core.errordownloading": "फ़ाइल डाउनलोड करने में त्रुटि।", + "core.errordownloadingsomefiles": "फ़ाइलों को डाउनलोड करने में त्रुटि। कुछ फाइलें गायब हो सकती हैं।", + "core.errorfileexistssamename": "इस नाम की एक फ़ाइल पहले से मौजूद है।", + "core.errorinvalidform": "फॉर्म में अमान्य डेटा है। कृपया जांचें कि सभी आवश्यक फ़ील्ड भरे हुए हैं और डेटा मान्य है।", + "core.errorinvalidresponse": "अमान्य प्रतिसाद प्राप्त हुआ। यदि त्रुटि बनी रहती है, तो कृपया अपने साइट व्यवस्थापक से संपर्क करें।", + "core.errorloadingcontent": "सामग्री लोड करने में त्रुटि।", + "core.errorofflinedisabled": "ऑफ़लाइन ब्राउज़िंग आपकी साइट पर अक्षम है। ऐप का उपयोग करने के लिए आपको इंटरनेट से जुड़ा होना चाहिए।", + "core.erroropenfilenoapp": "फ़ाइल खोलने में त्रुटि: इस प्रकार की फ़ाइल को खोलने के लिए कोई ऐप नहीं मिला।", + "core.erroropenfilenoextension": "फ़ाइल खोलने में त्रुटि: फ़ाइल में एक्सटेंशन नहीं है।", + "core.erroropenpopup": "यह गतिविधि पॉपअप खोलने का प्रयास कर रही है। यह एप्लिकेशन में समर्थित नहीं है।", + "core.errorrenamefile": "फ़ाइल का नाम बदलने में त्रुटि। कृपया पुन: प्रयास करें।", + "core.errorsync": "सिंक्रनाइज़ करते समय एक त्रुटि हुई। कृपया पुन: प्रयास करें।", + "core.errorsyncblocked": "यह {{$a}} अभी चल रही प्रक्रिया के कारण सिंक्रनाइज़ नहीं किया जा सकता है। बाद में पुन: प्रयास करें। यदि समस्या बनी रहती है, तो एप्लिकेशन को पुनरारंभ करने का प्रयास करें।", + "core.explanationdigitalminor": "यह जानकारी यह निर्धारित करने के लिए आवश्यक है कि आपकी उम्र सहमति की डिजिटल आयु से अधिक है या नहीं। यह वह उम्र है जब कोई व्यक्ति नियम और शर्तों और उनके डेटा को कानूनी रूप से संग्रहीत और संसाधित करने के लिए सहमति दे सकता है।", + "core.filenameexist": "फ़ाइल नाम पहले से मौजूद है: {{$a}}", + "core.fileuploader.audio": "ऑडियो", + "core.fileuploader.camera": "कैमरा", + "core.fileuploader.confirmuploadfile": "आप {{size}} अपलोड करने वाले हैं। क्या तुम वाकई जारी रखना चाहते हो?", + "core.fileuploader.confirmuploadunknownsize": "अपलोड के आकार की गणना करना संभव नहीं था। क्या तुम वाकई जारी रखना चाहते हो?", + "core.fileuploader.errorcapturingaudio": "ऑडियो कैप्चर करने में त्रुटि।", + "core.fileuploader.errorcapturingimage": "छवि कैप्चर करने में त्रुटि", + "core.fileuploader.errorcapturingvideo": "वीडियो कैप्चर करने में त्रुटि।", + "core.fileuploader.errorgettingimagealbum": "एल्बम से छवि प्राप्त करने में त्रुटि।", + "core.fileuploader.errormustbeonlinetoupload": "फाइल अपलोड करने के लिए आपको ऑनलाइन रहना होगा।", + "core.fileuploader.errornoapp": "इस क्रिया को करने के लिए आपके पास कोई ऐप इंस्टॉल नहीं है।", + "core.fileuploader.errorreadingfile": "फ़ाइल पढ़ने में त्रुटि।", + "core.fileuploader.errorwhileuploading": "फ़ाइल अपलोड के दौरान एक त्रुटि हुई।", + "core.fileuploader.file": "फ़ाइल", + "core.fileuploader.filesofthesetypes": "स्वीकृत फ़ाइल प्रकार:", + "core.fileuploader.fileuploaded": "फ़ाइल सफलतापूर्वक अपलोड कर दी गई थी।", + "core.fileuploader.maxbytesfile": "फ़ाइल {{$a.file}} बहुत बड़ी है। आपके द्वारा अपलोड किया जाने वाला अधिकतम आकार {{$a.size}} है।", + "core.fileuploader.photoalbums": "तस्वीर चित्राधार", + "core.fileuploader.readingfile": "फाइल पढ़ना", + "core.fileuploader.readingfileperc": "फ़ाइल पढ़ना: {{$a}}%", + "core.fileuploader.selectafile": "किसी फाइल का चयन करें", + "core.fileuploader.uploadafile": "एक फाइल अपलोड करें", + "core.fileuploader.uploading": "अपलोड हो रहा है", + "core.fileuploader.uploadingperc": "अपलोड करना: {{$a}}%", + "core.fileuploader.video": "वीडियो", + "core.folder": "फोल्डर", + "core.forcepasswordchangenotice": "आगे बढ़ने के लिए आपको अपना पासवर्ड बदलना होगा।", + "core.fulllistofcourses": "सभी कोर्सस", + "core.fullnameandsitename": "{{fullname}} ({{sitename}})", + "core.grades.average": "औसत", + "core.groupsseparate": "अलग ग्रूप्स", + "core.groupsvisible": "अलग ग्रूप्स", + "core.hasdatatosync": "इस {{$a}} समकालित होने के लिए ऑफ़लाइन डेटा है।", + "core.help": "सहायता", + "core.hide": "छिपादो", + "core.hour": "घंटा", + "core.hours": "घंट", + "core.humanreadablesize": "{{size}} {{unit}}", + "core.image": "छवि", + "core.imageviewer": "छवि दर्शक", + "core.info": "सूचना", + "core.ios": "आईओएस", + "core.lastaccess": "पिछ्ला आगमन", + "core.lastdownloaded": "अंतिम बार डाउनलोड किया गया", + "core.lastmodified": "पिछ्ला सुधार", + "core.lastsync": "अंतिम तुल्यकालन", + "core.list": "सूची", + "core.listsep": ";", + "core.loadmore": "और लोड करें", + "core.location": "स्थान", + "core.login.authenticating": "प्रमाणित कर रहा है", + "core.login.cancel": "कैन्सॅल", + "core.login.checksiteversion": "जांचें कि आपकी साइट Moodle 2.4 या बाद का उपयोग करती है।", + "core.login.confirmdeletesite": "क्या आप वाकई {{sitename}} साइट को हटाना चाहते हैं?", + "core.login.connect": "जुडिये!", + "core.login.connecttomoodle": "मूडल से कनेक्ट करें", + "core.login.contactyouradministrator": "आगे की मदद के लिए अपनी साइट के व्यवस्थापक से संपर्क करें।", + "core.login.contactyouradministratorissue": "कृपया निम्न समस्या की जाँच करने के लिए अपने साइट व्यवस्थापक से पूछें: {{$a}}", + "core.login.createaccount": "मेरा नया खाता बनाइए", + "core.login.createuserandpass": "लॉगिन करने के लिए यूज़रनेम और पास्वर्ड बनाइए", + "core.login.credentialsdescription": "कृपया लॉग इन करने के लिए अपना उपयोगकर्ता नाम और पासवर्ड प्रदान करें।", + "core.login.emailconfirmsent": "

आपके पते {{$a}}

पर एक ईमेल भेजा जाना चाहिए था\n

इसमें आपके पंजीकरण को पूरा करने के लिए आसान निर्देश हैं। \n

यदि आपको कुछ कठिनाई हो रही है, तो साइट व्यवस्थापक से संपर्क करें। ", + "core.login.emailconfirmsentnoemail": "

आपके पते पर एक ईमेल भेजा जाना चाहिए।

इसमें आपका पंजीकरण पूरा करने के लिए आसान निर्देश शामिल हैं।

यदि आपको कठिनाई जारी है, तो साइट व्यवस्थापक से संपर्क करें। ", + "core.login.emailconfirmsentsuccess": "पुष्टिकरण ईमेल सफलतापूर्वक भेजा गया", + "core.login.emailnotmatch": "ईमेल एक - दूसरे से मेल नहीं खाते", + "core.login.enterthewordsabove": "ऊपर्युक्त शब्दों को दर्ज करें", + "core.login.erroraccesscontrolalloworigin": "जिस क्रॉस-ऑरिजिन कॉल को आप करने की कोशिश कर रहे हैं, उसे अस्वीकार कर दिया गया है। कृपया https://docs.moodle.org/dev/Moodle_Mobile_development_using_Chrome_or_hhhium पर जाएं", + "core.login.errordeletesite": "इस साइट को हटाते समय एक त्रुटि हुई। कृपया पुन: प्रयास करें।", + "core.login.errorupdatesite": "साइट के टोकन को अपडेट करते समय एक त्रुटि हुई।", + "core.login.findyoursite": "अपनी साइट खोजें", + "core.login.firsttime": "क्या आप यहाँ पहली बार आयें हैं?", + "core.login.forgotten": "क्या आप अपना यूज़रनेम या पासवर्ड भूल गये हैं?", + "core.login.getanothercaptcha": "एक और कैप्चा प्राप्त करें", + "core.login.help": "सहायता", + "core.login.helpmelogin": "

दुनिया भर में कई हजारों Moodle साइट्स हैं। यह ऐप केवल उन Moodle साइटों से कनेक्ट हो सकता है जिन्होंने विशेष रूप से मोबाइल ऐप एक्सेस सक्षम किया है।

यदि आप अपनी Moodle साइट से कनेक्ट नहीं कर सकते हैं, तो आपको अपने साइट व्यवस्थापक से संपर्क करने और उन्हें पढ़ने के लिए कहने की आवश्यकता है \"http://docs.moodle.org/en/Mobile_app\" लक्ष्य = \"_blank\"> http://docs.moodle.org/en/Mobile_app

में एप्लिकेशन का परीक्षण करने के लिए Moodle डेमो साइट प्रकार शिक्षक या छात्र साइट पते फ़ील्ड में और कनेक्ट बटन ", + "core.login.instructions": "निर्देश", + "core.login.invalidaccount": "कृपया अपना लॉगिन विवरण जांचें या साइट कॉन्फ़िगरेशन की जांच करने के लिए अपने साइट व्यवस्थापक से पूछें।", + "core.login.invalidemail": "अमान्य ईमेल पता", + "core.login.invalidmoodleversion": "अमान्य Moodle संस्करण। आवश्यक न्यूनतम संस्करण 2.4 है।", + "core.login.invalidsite": "साइट URL अमान्य है।", + "core.login.invalidtime": "अमान्य समय", + "core.login.invalidvaluemax": "अधिकतम मूल्य {{$ a}} है", + "core.login.invalidvaluemin": "न्यूनतम मान {{$ a}} है", + "core.login.legacymoodleversion": "आप किसी असमर्थित Moodle संस्करण से कनेक्ट करने का प्रयास कर रहे हैं। कृपया, इस Moodle साइट तक पहुँचने के लिए Moodle क्लासिक ऐप डाउनलोड करें", + "core.login.legacymoodleversiondesktop": "आप {{$a}} से जुड़ने का प्रयास कर रहे हैं।

यह साइट Moodle का पुराना असमर्थित संस्करण चला रही है जो इस Moodle डेस्कटॉप ऐप के साथ काम नहीं करेगा।

br> यदि यह आपकी साइट है तो कृपया इसे अपडेट करने के लिए सहायता प्राप्त करने के लिए अपने स्थानीय मूडी साथी से संपर्क करें।

हमारे संपर्क पृष्ठ देखें सहायता के लिए अनुरोध प्रस्तुत करने के लिए।", + "core.login.legacymoodleversiondesktopdownloadold": "

वैकल्पिक रूप से, आप अभी भी इस साइट को ऐप के एक असमर्थित संस्करण का उपयोग करके एक्सेस कर सकते हैं जिसे यहां से डाउनलोड किया जा सकता है।", + "core.login.localmobileunexpectedresponse": "Moodle मोबाइल एडिशनल फीचर्स चेक ने अप्रत्याशित प्रतिक्रिया दी। आपको मानक मोबाइल सेवा का उपयोग करके प्रमाणित किया जाएगा।", + "core.login.loggedoutssodescription": "आपको फिर से प्रमाणित करना होगा। आपको ब्राउज़र विंडो में साइट पर लॉग इन करना होगा", + "core.login.login": "लॉग-इन", + "core.login.loginbutton": "लॉग इन करें", + "core.login.logininsiterequired": "आपको ब्राउज़र विंडो में साइट पर लॉग इन करना होगा।", + "core.login.loginsteps": "नमस्ते! पूर्ण प्रवेश के लिये आपको एक मिनट का समय निकाल कर इस सा‌इट पर अपना ऍका‌उन्ट बनना होगा। प्रत्येक कोर्स के लिये एक\n\"प्रवेश की\" भी हो सकती है, जिसकी आवश्यकता आपको बाद में\nपड़ सकती है। कृपया निम्नलिखित निर्देशों का पालन कीजिये:\n\n

    \n
  1. नये ऍकाउन्ट का फ़ॉर्म भरिये।
  2. \n\n
  3. हम आपको ई-मेल भेजेंगे।
  4. \n\n
  5. आप भेजे गए ई-मेल सन्देश को पढ़ने के बाद उसमें दी गई लिंक पर क्लिक करिए।
  6. \n\n
  7. आपके ऍकाउन्ट की पुष्टि होने के बाद आप लॉग-इन कर सकेंगे।
  8. \n\n
  9. अब आप अपना मनपसन्द कोर्स चुन सकते हैं।
  10. \n\n
  11. आगर कोर्स के लिये \"प्रवेश की\" की आवश्यकता है, तो अपने\nटीचर द्वारा दी गयी \"प्रवेश की\" क उपयोग करें. आप कोर्स में प्रवेश\nपा सकेंगे।
  12. \n\n
  13. अब आप कोर्स में पूर्ण प्रवेश पा सकेंगे. अब से आपको अपने कोर्स में प्रवेश पाने के लिये केवल अपना यूज़रनेम और पासवर्ड देना गा।
  14. \n\n
", + "core.login.missingemail": "ई-मेल गायब", + "core.login.missingfirstname": "प्रथम नाम गायब", + "core.login.missinglastname": "उपनाम गायब", + "core.login.mobileservicesnotenabled": "आपकी साइट पर मोबाइल पहुंच सक्षम नहीं है। कृपया अपने साइट व्यवस्थापक से संपर्क करें यदि आपको लगता है कि इसे सक्षम किया जाना चाहिए।", + "core.login.newaccount": "नया ऍकाउन्ट", + "core.login.newsitedescription": "कृपया अपनी Moodle साइट का URL दर्ज करें। ध्यान दें कि इस ऐप के साथ काम करने के लिए इसे कॉन्फ़िगर नहीं किया जा सकता है।", + "core.login.notloggedin": "आपको लॉग इन करने की जरूरत है।", + "core.login.password": "पासवर्ड", + "core.login.passwordrequired": "पासवर्ड आवश्यक", + "core.login.problemconnectingerror": "हमें कनेक्ट करने में समस्या हो रही है", + "core.login.problemconnectingerrorcontinue": "दोबारा जांच करें कि आपने पता सही दर्ज किया है और पुनः प्रयास करें।", + "core.login.recaptchachallengeimage": "reCAPTCHA चुनौती छवि", + "core.login.recaptchaexpired": "सत्यापन समाप्त हो गया। सुरक्षा प्रश्न का उत्तर फिर से दें।", + "core.login.recaptchaincorrect": "सुरक्षा प्रश्न उत्तर गलत है।", + "core.login.reconnect": "फिर से कनेक्ट करें", + "core.login.reconnectdescription": "आपका प्रमाणीकरण टोकन अमान्य है या समाप्त हो चुका है। आपको साइट को फिर से कनेक्ट करना होगा।", + "core.login.reconnectssodescription": "आपका प्रमाणीकरण टोकन अमान्य है या समाप्त हो चुका है। आपको साइट को फिर से कनेक्ट करना होगा। आपको ब्राउज़र विंडो में साइट पर लॉग इन करना होगा।", + "core.login.searchby": "खोज से:", + "core.login.selectacountry": "एक देश को चुनिए", + "core.login.selectsite": "Please select your site:", + "core.login.signupplugindisabled": "{{$ a}} सक्षम नहीं है।", + "core.login.siteaddress": "साइट का पता", + "core.login.sitehasredirect": "आपकी साइट में कम से कम एक HTTP पुनर्निर्देशित है। ऐप पुनर्निर्देशित नहीं कर सकता है, यह समस्या हो सकती है जो ऐप को आपकी साइट से कनेक्ट करने से रोक रही है।", + "core.login.siteinmaintenance": "आपकी साइट रखरखाव मोड में है", + "core.login.sitepolicynotagreederror": "साइट नीति सहमत नहीं है।", + "core.login.siteurl": "साईट यूआरएल", + "core.login.siteurlrequired": "साइट URL की आवश्यकता है i.e http://www.yourmoodlesite.org ", + "core.login.startsignup": "नया ऍकाउन्ट बनाइये", + "core.login.stillcantconnect": "अभी भी नहीं जुड़ सकता है?", + "core.login.username": "यूज़रनेम", + "core.login.usernameoremail": "यूज़रनेम या ईमेल दर्ज करें", + "core.login.usernamerequired": "उपयोगकर्ता का नाम (आवश्यक", + "core.login.visitchangepassword": "क्या आप पासवर्ड बदलने के लिए साइट पर जाना चाहते हैं?", + "core.login.webservicesnotenabled": "आपकी साइट में वेब सेवाएँ सक्षम नहीं हैं। कृपया अपने साइट व्यवस्थापक से संपर्क करें यदि आपको लगता है कि उन्हें सक्षम होना चाहिए।", + "core.lostconnection": "आपका प्रमाणीकरण टोकन अमान्य है या समाप्त हो चुका है। आपको साइट को फिर से कनेक्ट करना होगा।", + "core.mainmenu.appsettings": "एप्लिकेशन सेटिंग", + "core.mainmenu.changesite": "साइट बदलें", + "core.mainmenu.help": "सहायता", + "core.mainmenu.logout": "लॉग ऑउट", + "core.mainmenu.website": "वेबसाइट", + "core.maxsizeandattachments": "नई फ़ाइलों के लिए अधिकतम आकार: {$ a->size}}, अधिकतम संलग्नक: {{$a.attachments}}", + "core.min": "मिनट", + "core.mins": "मिनट", + "core.mod_chat": "चैट", + "core.mod_file": "फ़ाइल", + "core.mod_forum": "फ़ोरम", + "core.mod_label": "लेबल", + "core.mod_workshop": "वर्कशॉप", + "core.more": "और", + "core.name": "नाम", + "core.networkerroriframemsg": "यह सामग्री ऑफ़लाइन उपलब्ध नहीं है। कृपया इंटरनेट से कनेक्ट करें और पुनः प्रयास करें।", + "core.networkerrormsg": "साइट से कनेक्ट करने में समस्या थी। कृपया अपने संपर्क की जांच करे और फिर से प्रयास करें।", + "core.never": "कभी नहीं", + "core.next": "अगला", + "core.no": "नहीं", + "core.nograde": "कोई ग्रेड नही", + "core.none": "कोई नहीं", + "core.nopasswordchangeforced": "आप अपना पासवर्ड बदले बिना आगे नहीं बढ़ सकते।", + "core.nopermissionerror": "क्षमा करें, लेकिन आपके पास वर्तमान में ऐसा करने की अनुमति नहीं है", + "core.noresults": "परिणाम", + "core.notapplicable": "n/a", + "core.notsent": "नहीं भेजा गया", + "core.numwords": "{{$a}} शब्द", + "core.offline": "ऑफ़लाइन", + "core.ok": "ठीक", + "core.online": "ऑनलाइन", + "core.openfullimage": "पूर्ण आकार की छवि प्रदर्शित करने के लिए यहां क्लिक करें", + "core.openinbrowser": "ब्राउज़र में खोलें", + "core.percentagenumber": "{{$a}}%", + "core.phone": "टेलीफोन", + "core.pictureof": "{{$a}} का चित्र", + "core.pulltorefresh": "रीफ़्रेश करने के लिए खींचें", + "core.question.answer": "उत्तर", + "core.question.cannotdeterminestatus": "स्थिति का निर्धारण नहीं किया जा सकता", + "core.question.complete": "पूरा", + "core.question.correct": "सही", + "core.question.errorattachmentsnotsupported": "एप्लिकेशन उत्तर देने के लिए फ़ाइलों को संलग्न करने का समर्थन नहीं करता है।", + "core.question.errorinlinefilesnotsupported": "एप्लिकेशन अभी तक इनलाइन फ़ाइलों के संपादन का समर्थन नहीं करता है।", + "core.question.errorquestionnotsupported": "यह प्रश्न प्रकार ऐप द्वारा समर्थित नहीं है: {{$a}}", + "core.question.howtodraganddrop": "चुनने के लिए टैप करें फिर ड्रॉप करने के लिए टैप करें।", + "core.question.questionmessage": "प्रश्न {{$a}}: {{$b}}", + "core.redirectingtosite": "आपको साइट पर पुनः निर्देशित किया जाएगा।", + "core.refresh": "ताज़ा करना", + "core.remove": "निकले", + "core.required": "ज़रूरी हैं", + "core.requireduserdatamissing": "इस उपयोगकर्ता के पास कुछ आवश्यक प्रोफ़ाइल डेटा का अभाव है। कृपया अपनी साइट में डेटा दर्ज करें और पुनः प्रयास करें।
{{$a}}", + "core.resources": "संसाधने", + "core.restore": "रिस्टोर", + "core.retry": "पुन: प्रयास करें", + "core.search": "खोज", + "core.searching": "खोज कर", + "core.searchresults": "खोज परिणाम", + "core.sec": "सेकेंड", + "core.secs": "सेकेंड्स", + "core.settings.about": "के बारे में", + "core.settings.appready": "ऐप तैयार है", + "core.settings.cannotsyncoffline": "ऑफ़लाइन सिंक्रनाइज़ नहीं किया जा सकता।", + "core.settings.cannotsyncwithoutwifi": "समन्‍वयित नहीं किया जा सकता है क्‍योंकि वर्तमान सेटिंग केवल वाई-फाई से कनेक्‍ट होने पर समन्‍वयित करने की अनुमति देती है। कृपया वाई-फाई नेटवर्क से कनेक्ट करें।", + "core.settings.compilationinfo": "संकलन की जानकारी", + "core.settings.cordovadevicemodel": "कॉर्डोवा डिवाइस मॉडल", + "core.settings.cordovadeviceosversion": "कॉर्डोवा डिवाइस ओएस संस्करण", + "core.settings.cordovadeviceplatform": "कॉर्डोवा डिवाइस प्लेटफॉर्म", + "core.settings.cordovadeviceuuid": "Cordova device UUID", + "core.settings.cordovaversion": "कॉर्डोवा संस्करण", + "core.settings.currentlanguage": "वर्तमान भाषा", + "core.settings.debugdisplaydescription": "यदि सक्षम किया गया है, तो त्रुटि मोडल्यूस संभव हो तो त्रुटि के बारे में अधिक डेटा प्रदर्शित करेगा।", + "core.settings.deletesitefiles": "क्या आप वाकई '{{sitename}}' साइट से डाउनलोड की गई फ़ाइलों को हटाना चाहते हैं?", + "core.settings.deletesitefilestitle": "साइट फ़ाइलें हटाएं", + "core.settings.deviceinfo": "यंत्र की जानकारी", + "core.settings.deviceos": "डिवाइस ओएस", + "core.settings.devicewebworkers": "डिवाइस वेब कार्यकर्ताओं ने समर्थन किया", + "core.settings.displayformat": "प्रारूप को प्रदर्शित करें", + "core.settings.enabledownloadsection": "डाउनलोड अनुभाग सक्षम करें", + "core.settings.enablerichtexteditor": "पाठ संपादक सक्षम करें", + "core.settings.enablerichtexteditordescription": "यदि सक्षम है, तो सामग्री दर्ज करते समय एक पाठ संपादक उपलब्ध होगा।", + "core.settings.enablesyncwifi": "वाई-फाई पर केवल तब ही सिंक करने की अनुमति दें", + "core.settings.errordeletesitefiles": "साइट फ़ाइलों को हटाने में त्रुटि।", + "core.settings.errorsyncsite": "साइट डेटा सिंक्रनाइज़ करने में त्रुटि। अपने इंटरनेट कनेक्शन की जाँच करें और पुन: प्रयास करें।", + "core.settings.estimatedfreespace": "अनुमानित खाली स्थान", + "core.settings.filesystemroot": "फ़ाइल सिस्टम रूट", + "core.settings.general": "सामान्य", + "core.settings.language": "भाषा", + "core.settings.license": "GPL लाइसेन्स", + "core.settings.localnotifavailable": "स्थानीय सूचनाएं उपलब्ध हैं", + "core.settings.locationhref": "वेब व्यू URL", + "core.settings.loggedin": "ऑनलाइन", + "core.settings.loggedoff": "ऑनलाइन नहीं", + "core.settings.navigatorlanguage": "नेविगेटर भाषा", + "core.settings.navigatoruseragent": "नेविगेटर userAgent", + "core.settings.networkstatus": "Internet connection status", + "core.settings.privacypolicy": "गोपनीयता नीति", + "core.settings.reportinbackground": "त्रुटियों की रिपोर्ट स्वचालित रूप से करें", + "core.settings.settings": "सेट्टिंग्स", + "core.settings.showdownloadoptions": "डाउनलोड विकल्प दिखाएं", + "core.settings.sites": "साईट्स", + "core.settings.spaceusage": "अंतरिक्ष उपयोग", + "core.settings.synchronization": "तुल्यकालन", + "core.settings.synchronizenow": "अब सिंक्रनाइज़ करें", + "core.settings.syncsettings": "सिंक्रनाइज़ेशन सेटिंग्स", + "core.settings.total": "कुल", + "core.settings.versioncode": "संस्करण कोड", + "core.settings.versionname": "संस्करण का नाम", + "core.settings.wificonnection": "वाईफाई कनेक्शन", + "core.sharedfiles.chooseaccountstorefile": "फ़ाइल को स्टोर करने के लिए एक खाता चुनें।", + "core.sharedfiles.chooseactionrepeatedfile": "इस नाम की एक फ़ाइल पहले से मौजूद है। क्या आप मौजूदा फ़ाइल को बदलना चाहते हैं या इसे \"{{$a}}\" नाम देना चाहते हैं?", + "core.sharedfiles.errorreceivefilenosites": "There are no sites stored. Please add a site before sharing a file with the app.", + "core.sharedfiles.nosharedfiles": "इस साइट पर कोई साझा फ़ाइलें संग्रहीत नहीं हैं।", + "core.sharedfiles.nosharedfilestoupload": "आपके पास यहां अपलोड करने के लिए कोई फाइल नहीं है। यदि आप किसी अन्य ऐप से फ़ाइल अपलोड करना चाहते हैं, तो फ़ाइल का पता लगाएं और 'ओपन' बटन पर क्लिक करें।", + "core.sharedfiles.rename": "नाम बदलें", + "core.sharedfiles.replace": "बदलने के", + "core.sharedfiles.sharedfiles": "फ़ाइलें साझा की हैं", + "core.sharedfiles.successstorefile": "फ़ाइल सफलतापूर्वक संग्रहीत की गई। अपनी निजी फ़ाइलों को अपलोड करने के लिए फ़ाइल का चयन करें या किसी गतिविधि में उपयोग करें।", + "core.show": "दिखाए", + "core.showmore": "अधिक दिखाएं...", + "core.site": "साइट", + "core.sitehome.sitenews": "साइट समाचार", + "core.sizeb": "बाइट्स", + "core.sizegb": "GB", + "core.sizekb": "KB", + "core.sizemb": "MB", + "core.sizetb": "TB", + "core.sorry": "माफ़ कीजिये...", + "core.strftimedate": "%d %B %Y", + "core.strftimedateshort": "%d %B", + "core.strftimedatetime": "%d %B %Y, %I:%M %p", + "core.strftimedatetimeshort": "%d/%m/%Y %H:%M", + "core.strftimedaydate": "%A, %d %B %Y", + "core.strftimedaydatetime": "%A, %d %B %Y, %I:%M %p", + "core.strftimedayshort": "%A, %d %B", + "core.strftimedaytime": "%a, %H:%M", + "core.strftimemonthyear": "%B %Y", + "core.strftimerecent": "%d %b, %H:%M", + "core.strftimerecentfull": "%a, %d %b %Y, %I:%M %p", + "core.strftimetime": "%I:%M %p", + "core.tablet": "Tablet", + "core.teachers": "अध्यपके", + "core.thereisdatatosync": "सिंक्रनाइज़ होने के लिए ऑफ़लाइन {{$a}} हैं।", + "core.thisdirection": "ltr", + "core.time": "समय", + "core.today": "आज", + "core.tryagain": "पुनः प्रयास करें", + "core.twoparagraphs": "{{p1}}

{{p2}}", + "core.uhoh": "उह ओह!", + "core.unexpectederror": "अप्रत्याशित त्रुटि। कृपया आवेदन को बंद करें और फिर से खोलें।", + "core.unicodenotsupported": "इस साइट पर कुछ इमोजीस समर्थित नहीं हैं। संदेश भेजे जाने पर ऐसे वर्ण हटा दिए जाएंगे।", + "core.unicodenotsupportedcleanerror": "यूनिकोड वर्णों की सफाई करते समय खाली पाठ पाया गया।", + "core.unknown": "अनजान", + "core.unlimited": "असीमित", + "core.unzipping": "अनज़िप", + "core.user.address": "पता", + "core.user.city": "शहर/गाँव", + "core.user.contact": "संपर्क करें", + "core.user.country": "देश", + "core.user.description": "विवरण", + "core.user.details": "विस्तार", + "core.user.detailsnotavailable": "इस उपयोगकर्ता का विवरण आपके लिए उपलब्ध नहीं है।", + "core.user.editingteacher": "टीचर", + "core.user.email": "ई-मेल पता", + "core.user.emailagain": "ई-मेल (दोबारा)", + "core.user.errorloaduser": "उपयोगकर्ता लोड करने में त्रुटि।", + "core.user.firstname": "प्रथम नाम", + "core.user.interests": "रुचिया", + "core.user.lastname": "सरनेम", + "core.user.newpicture": "नया चित्र", + "core.user.phone1": "टेलीफोन", + "core.user.roles": "भूमिकाएं", + "core.user.sendemail": "ईमेल", + "core.user.student": "विद्यार्थी", + "core.user.webpage": "वेब पेज", + "core.userdeleted": "यह यूज़र ऍकाउन्ट डिलीट कर दिया गया है", + "core.userdetails": "उपयोगकर्ता का विवरण", + "core.users": "उपयोगकर्ता", + "core.view": "देखें", + "core.viewcode": "कोड देखें", + "core.vieweditor": "संपादक देखें", + "core.viewembeddedcontent": "एम्बेडेड सामग्री देखें", + "core.viewprofile": "प्रोफ़ाइल देखें", + "core.warningofflinedatadeleted": "{{componenet}} '{{name}}' से ऑफ़लाइन डेटा हटा दिया गया है। {{error}}", + "core.whoops": "Oops!", + "core.whyisthishappening": "ये क्यों हो रहा है?", + "core.windowsphone": "विंडोज फ़ोन", + "core.wsfunctionnotavailable": "वेब सेवा फ़ंक्शन उपलब्ध नहीं है।", + "core.year": "साल", + "core.years": "सालो", + "core.yes": "हाँ" +} \ No newline at end of file diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index 2d1106973..d247783da 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -46,9 +46,6 @@ export class CoreSyncBaseProvider { // Store sync promises. protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise } } = {}; - // List of services that will be injected using injector. - // It's done like this so subclasses don't have to send all the services to the parent in the constructor. - constructor(component: string, loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService, @@ -243,7 +240,7 @@ export class CoreSyncBaseProvider { if (!siteId) { // No site ID defined, sync all sites. this.logger.debug(`Try to sync '${syncFunctionLog}' in all sites.`); - promise = this.sitesProvider.getSitesIds(); + promise = this.sitesProvider.getLoggedInSitesIds(); } else { this.logger.debug(`Try to sync '${syncFunctionLog}' in site '${siteId}'.`); promise = Promise.resolve([siteId]); diff --git a/src/classes/modal-lateral-transition.ts b/src/classes/modal-lateral-transition.ts new file mode 100644 index 000000000..69d4db3c0 --- /dev/null +++ b/src/classes/modal-lateral-transition.ts @@ -0,0 +1,72 @@ +// (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 { Animation } from 'ionic-angular/animations/animation'; +import { PageTransition } from 'ionic-angular/transitions/page-transition'; + +/** + * Sliding transition for lateral modals. + */ +export class CoreModalLateralTransition extends PageTransition { + /** + * Animation. + */ + init(): void { + const enteringView = this.enteringView; + const leavingView = this.leavingView; + + const plt = this.plt; + const OFF_RIGHT = plt.isRTL ? '-100%' : '100%'; + + if (enteringView && enteringView.pageRef()) { + const ele = enteringView.pageRef().nativeElement; + const wrapper = new Animation(this.plt, ele.querySelector('.modal-wrapper')); + const backdrop = new Animation(this.plt, ele.querySelector('ion-backdrop')); + + wrapper.beforeStyles({ transform: 'translateX(' + OFF_RIGHT + ')', opacity: 0.8 }); + wrapper.fromTo('transform', 'translateX(' + OFF_RIGHT + ')', 'translateX(0)'); + wrapper.fromTo('opacity', 0.8, 1); + backdrop.fromTo('opacity', 0.01, 0.4); + + this + .element(enteringView.pageRef()) + .duration(300) + .easing('cubic-bezier(0.36,0.66,0.04,1)') + .add(wrapper) + .add(backdrop); + } + + if (leavingView && leavingView.pageRef()) { + const ele = this.leavingView.pageRef().nativeElement; + const wrapper = new Animation(this.plt, ele.querySelector('.modal-wrapper')); + const contentWrapper = new Animation(this.plt, ele.querySelector('.wrapper')); + const backdrop = new Animation(this.plt, ele.querySelector('ion-backdrop')); + + wrapper.beforeStyles({ transform: 'translateX(0)', opacity: 1 }); + wrapper.fromTo('transform', 'translateX(0)', 'translateX(' + OFF_RIGHT + ')'); + wrapper.fromTo('opacity', 1, 0.8); + contentWrapper.fromTo('opacity', 1, 0); + backdrop.fromTo('opacity', 0.4, 0); + + this + .element(leavingView.pageRef()) + .duration(300) + .easing('cubic-bezier(0.36,0.66,0.04,1)') + .add(contentWrapper) + .add(wrapper) + .add(backdrop); + + } + } +} diff --git a/src/classes/site.ts b/src/classes/site.ts index 08b3512da..b9068bd13 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -166,47 +166,8 @@ export class CoreSite { protected wsProvider: CoreWSProvider; // Variables for the database. - protected WS_CACHE_TABLE = 'wscache'; - protected CONFIG_TABLE = 'core_site_config'; - protected tableSchemas = [ - { - name: this.WS_CACHE_TABLE, - columns: [ - { - name: 'id', - type: 'TEXT', - primaryKey: true - }, - { - name: 'data', - type: 'TEXT' - }, - { - name: 'key', - type: 'TEXT' - }, - { - name: 'expirationTime', - type: 'INTEGER' - } - ] - }, - { - name: this.CONFIG_TABLE, - columns: [ - { - name: 'name', - type: 'TEXT', - unique: true, - notNull: true - }, - { - name: 'value' - } - ] - } - - ]; + static WS_CACHE_TABLE = 'wscache'; + static CONFIG_TABLE = 'core_site_config'; // Versions of Moodle releases. protected MOODLE_RELEASES = { @@ -224,6 +185,7 @@ export class CoreSite { protected cleanUnicode = false; protected lastAutoLogin = 0; protected offlineDisabled = false; + protected ongoingRequests: { [cacheId: string]: Promise } = {}; /** * Create a site. @@ -267,7 +229,6 @@ export class CoreSite { */ initDB(): void { this.db = this.dbProvider.getDB('Site-' + this.id); - this.db.createTablesFromSchema(this.tableSchemas); } /** @@ -611,15 +572,24 @@ export class CoreSite { return Promise.reject(this.utils.createFakeWSError('core.unicodenotsupportedcleanerror', true)); } - return this.getFromCache(method, data, preSets, false, originalData).catch(() => { + const cacheId = this.getCacheId(method, data); + + // Check for an ongoing identical request if we're not ignoring cache. + if (preSets.getFromCache && this.ongoingRequests[cacheId]) { + return this.ongoingRequests[cacheId].then((response) => { + // Clone the data, this may prevent errors if in the callback the object is modified. + return this.utils.clone(response); + }); + } + + const promise = this.getFromCache(method, data, preSets, false, originalData).catch(() => { // Do not pass those options to the core WS factory. return this.wsProvider.call(method, data, wsPreSets).then((response) => { if (preSets.saveToCache) { this.saveToCache(method, data, response, preSets); } - // We pass back a clone of the original object, this may prevent errors if in the callback the object is modified. - return this.utils.clone(response); + return response; }).catch((error) => { if (error.errorcode == 'invalidtoken' || (error.errorcode == 'accessexception' && error.message.indexOf('Invalid token - token expired') > -1)) { @@ -635,7 +605,7 @@ export class CoreSite { // Session expired, trigger event. this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {}, this.id); - // Change error message. We'll try to get data from cache. + // Change error message. Try to get data from cache, the event will handle the error. error.message = this.translate.instant('core.lostconnection'); } else if (error.errorcode === 'userdeleted') { // User deleted, trigger event. @@ -644,17 +614,15 @@ export class CoreSite { return Promise.reject(error); } else if (error.errorcode === 'forcepasswordchangenotice') { - // Password Change Forced, trigger event. + // Password Change Forced, trigger event. Try to get data from cache, the event will handle the error. this.eventsProvider.trigger(CoreEventsProvider.PASSWORD_CHANGE_FORCED, {}, this.id); error.message = this.translate.instant('core.forcepasswordchangenotice'); - return Promise.reject(error); } else if (error.errorcode === 'usernotfullysetup') { - // User not fully setup, trigger event. + // User not fully setup, trigger event. Try to get data from cache, the event will handle the error. this.eventsProvider.trigger(CoreEventsProvider.USER_NOT_FULLY_SETUP, {}, this.id); error.message = this.translate.instant('core.usernotfullysetup'); - return Promise.reject(error); } else if (error.errorcode === 'sitepolicynotagreed') { // Site policy not agreed, trigger event. this.eventsProvider.trigger(CoreEventsProvider.SITE_POLICY_NOT_AGREED, {}, this.id); @@ -672,10 +640,13 @@ export class CoreSite { error.message = this.translate.instant('core.unicodenotsupported'); return Promise.reject(error); - } else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission') { + } else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission' || + error.errorcode === 'notingroup') { + // Translate error messages with missing strings. if (error.message === 'error/nopermission') { - // This error message is returned by some web services but the string does not exist. error.message = this.translate.instant('core.nopermissionerror'); + } else if (error.message === 'error/notingroup') { + error.message = this.translate.instant('core.notingroup'); } // Save the error instead of deleting the cache entry so the same content is displayed in offline. @@ -713,6 +684,19 @@ export class CoreSite { return response; }); + + this.ongoingRequests[cacheId] = promise; + + // Clear ongoing request after setting the promise (just in case it's already resolved). + return promise.finally(() => { + // Make sure we don't clear the promise of a newer request that ignores the cache. + if (this.ongoingRequests[cacheId] === promise) { + delete this.ongoingRequests[cacheId]; + } + }).then((response) => { + // We pass back a clone of the original object, this may prevent errors if in the callback the object is modified. + return this.utils.clone(response); + }); } /** @@ -784,10 +768,10 @@ export class CoreSite { let promise; if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) { - promise = this.db.getRecords(this.WS_CACHE_TABLE, { key: preSets.cacheKey }).then((entries) => { + promise = this.db.getRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey }).then((entries) => { if (!entries.length) { // Cache key not found, get by params sent. - return this.db.getRecord(this.WS_CACHE_TABLE, { id: id }); + return this.db.getRecord(CoreSite.WS_CACHE_TABLE, { id: id }); } else if (entries.length > 1) { // More than one entry found. Search the one with same ID as this call. for (let i = 0, len = entries.length; i < len; i++) { @@ -801,13 +785,13 @@ export class CoreSite { return entries[0]; }); } else { - promise = this.db.getRecord(this.WS_CACHE_TABLE, { id: id }).catch(() => { + promise = this.db.getRecord(CoreSite.WS_CACHE_TABLE, { id: id }).catch(() => { // Entry not found, try to get it using the old ID. const oldId = this.getCacheOldId(method, originalData || {}); - return this.db.getRecord(this.WS_CACHE_TABLE, { id: oldId }).then((entry) => { + return this.db.getRecord(CoreSite.WS_CACHE_TABLE, { id: oldId }).then((entry) => { // Update the entry ID to use the new one. - this.db.updateRecords(this.WS_CACHE_TABLE, {id: id}, {id: oldId}); + this.db.updateRecords(CoreSite.WS_CACHE_TABLE, {id: id}, {id: oldId}); return entry; }); @@ -877,7 +861,7 @@ export class CoreSite { entry.key = preSets.cacheKey; } - return this.db.insertRecord(this.WS_CACHE_TABLE, entry); + return this.db.insertRecord(CoreSite.WS_CACHE_TABLE, entry); }); } @@ -898,10 +882,10 @@ export class CoreSite { const id = this.getCacheId(method, data); if (allCacheKey) { - return this.db.deleteRecords(this.WS_CACHE_TABLE, { key: preSets.cacheKey }); + return this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey }); } - return this.db.deleteRecords(this.WS_CACHE_TABLE, { id: id }); + return this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { id: id }); } /* @@ -935,7 +919,7 @@ export class CoreSite { this.logger.debug('Invalidate all the cache for site: ' + this.id); - return this.db.updateRecords(this.WS_CACHE_TABLE, { expirationTime: 0 }); + return this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 }); } /** @@ -954,7 +938,7 @@ export class CoreSite { this.logger.debug('Invalidate cache for key: ' + key); - return this.db.updateRecords(this.WS_CACHE_TABLE, { expirationTime: 0 }, { key: key }); + return this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 }, { key: key }); } /** @@ -997,7 +981,7 @@ export class CoreSite { this.logger.debug('Invalidate cache for key starting with: ' + key); - const sql = 'UPDATE ' + this.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?'; + const sql = 'UPDATE ' + CoreSite.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?'; return this.db.execute(sql, [key + '%']); } @@ -1590,7 +1574,7 @@ export class CoreSite { * @return {Promise} Promise resolved when done. */ deleteSiteConfig(name: string): Promise { - return this.db.deleteRecords(this.CONFIG_TABLE, { name: name }); + return this.db.deleteRecords(CoreSite.CONFIG_TABLE, { name: name }); } /** @@ -1601,7 +1585,7 @@ export class CoreSite { * @return {Promise} Resolves upon success along with the config data. Reject on failure. */ getLocalSiteConfig(name: string, defaultValue?: any): Promise { - return this.db.getRecord(this.CONFIG_TABLE, { name: name }).then((entry) => { + return this.db.getRecord(CoreSite.CONFIG_TABLE, { name: name }).then((entry) => { return entry.value; }).catch((error) => { if (typeof defaultValue != 'undefined') { @@ -1620,6 +1604,6 @@ export class CoreSite { * @return {Promise} Promise resolved when done. */ setLocalSiteConfig(name: string, value: number | string): Promise { - return this.db.insertRecord(this.CONFIG_TABLE, { name: name, value: value }); + return this.db.insertRecord(CoreSite.CONFIG_TABLE, { name: name, value: value }); } } diff --git a/src/classes/sqlitedb.ts b/src/classes/sqlitedb.ts index 8e650767b..9bc24272b 100644 --- a/src/classes/sqlitedb.ts +++ b/src/classes/sqlitedb.ts @@ -15,6 +15,129 @@ import { SQLite, SQLiteObject } from '@ionic-native/sqlite'; import { Platform } from 'ionic-angular'; +/** + * Schema of a table. + */ +export interface SQLiteDBTableSchema { + /** + * The table name. + * @type {string} + */ + name: string; + + /** + * The columns to create in the table. + * @type {SQLiteDBColumnSchema[]} + */ + columns: SQLiteDBColumnSchema[]; + + /** + * Names of columns that are primary key. Use it for compound primary keys. + * @type {string[]} + */ + primaryKeys?: string[]; + + /** + * List of sets of unique columns. E.g: [['section', 'title'], ['author', 'title']]. + * @type {string[][]} + */ + uniqueKeys?: string[][]; + + /** + * List of foreign keys. + * @type {SQLiteDBForeignKeySchema[]} + */ + foreignKeys?: SQLiteDBForeignKeySchema[]; + + /** + * Check constraint for the table. + * @type {string} + */ + tableCheck?: string; +} + +/** + * Schema of a column. + */ +export interface SQLiteDBColumnSchema { + /** + * Column's name. + * @type {string} + */ + name: string; + + /** + * Column's type. + * @type {string} + */ + type?: 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB'; + + /** + * Whether the column is a primary key. Use it only if primary key is a single column. + * @type {boolean} + */ + primaryKey?: boolean; + + /** + * Whether it should be autoincremented. Only if primaryKey is true. + * @type {boolean} + */ + autoIncrement?: boolean; + + /** + * True if column shouldn't be null. + * @type {boolean} + */ + notNull?: boolean; + + /** + * WWhether the column is unique. + * @type {boolean} + */ + unique?: boolean; + + /** + * Check constraint for the column. + * @type {string} + */ + check?: string; + + /** + * Default value for the column. + * @type {string} + */ + default?: string; +} + +/** + * Schema of a foreign key. + */ +export interface SQLiteDBForeignKeySchema { + /** + * Columns to include in this foreign key. + * @type {string[]} + */ + columns: string[]; + + /** + * The external table referenced by this key. + * @type {string} + */ + table: string; + + /** + * List of referenced columns from the referenced table. + * @type {string[]} + */ + foreignColumns?: string[]; + + /** + * Text with the actions to apply to the foreign key. + * @type {string} + */ + actions?: string; +} + /** * Class to interact with the local database. * @@ -43,27 +166,15 @@ export class SQLiteDB { * Helper function to create a table if it doesn't exist. * * @param {string} name The table name. - * @param {any[]} columns The columns to create in the table. Each column can have: - * * {string} name Column's name. - * * {string} [type] Column's type. - * * {boolean} [primaryKey] If column is primary key. Use it only if primary key is a single column. - * * {boolean} [autoIncrement] Whether it should be autoincremented. Only if primaryKey is true. - * * {boolean} [notNull] True if column shouldn't be null. - * * {boolean} [unique] Whether the column is unique. - * * {string} [check] Check constraint for the column. - * * {string} [default] Default value for the column. + * @param {SQLiteDBColumnSchema[]} columns The columns to create in the table. * @param {string[]} [primaryKeys] Names of columns that are primary key. Use it for compound primary keys. * @param {string[][]} [uniqueKeys] List of sets of unique columns. E.g: [['section', 'title'], ['author', 'title']]. - * @param {any[]} [foreignKeys] List of foreign keys. Each key can have: - * * {string[]} columns Columns to include in this foreign key. - * * {string} table The external table referenced by this key. - * * {string[]} [foreignColumns] List of referenced columns from the referenced table. - * * {string} [actions] Text with the actions to apply to the foreign key. + * @param {SQLiteDBForeignKeySchema[]} [foreignKeys] List of foreign keys. * @param {string} [tableCheck] Check constraint for the table. * @return SQL query. */ - buildCreateTableSql(name: string, columns: any[], primaryKeys?: string[], uniqueKeys?: string[][], foreignKeys?: any[], - tableCheck?: string): string { + buildCreateTableSql(name: string, columns: SQLiteDBColumnSchema[], primaryKeys?: string[], uniqueKeys?: string[][], + foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string): string { const columnsSql = []; let sql = `CREATE TABLE IF NOT EXISTS ${name} (`; @@ -207,27 +318,15 @@ export class SQLiteDB { * Create a table if it doesn't exist. * * @param {string} name The table name. - * @param {any[]} columns The columns to create in the table. Each column can have: - * * {string} name Column's name. - * * {string} [type] Column's type. - * * {boolean} [primaryKey] If column is primary key. Use it only if primary key is a single column. - * * {boolean} [autoIncrement] Whether it should be autoincremented. Only if primaryKey is true. - * * {boolean} [notNull] True if column shouldn't be null. - * * {boolean} [unique] Whether the column is unique. - * * {string} [check] Check constraint for the column. - * * {string} [default] Default value for the column. + * @param {SQLiteDBColumnSchema[]} columns The columns to create in the table. * @param {string[]} [primaryKeys] Names of columns that are primary key. Use it for compound primary keys. * @param {string[][]} [uniqueKeys] List of sets of unique columns. E.g: [['section', 'title'], ['author', 'title']]. - * @param {any[]} [foreignKeys] List of foreign keys. Each key can have: - * * {string[]} columns Columns to include in this foreign key. - * * {string} table The external table referenced by this key. - * * {string[]} [foreignColumns] List of referenced columns from the referenced table. - * * {string} [actions] Text with the actions to apply to the foreign key. + * @param {SQLiteDBForeignKeySchema[]} [foreignKeys] List of foreign keys. * @param {string} [tableCheck] Check constraint for the table. * @return {Promise} Promise resolved when success. */ - createTable(name: string, columns: any[], primaryKeys?: string[], uniqueKeys?: string[][], foreignKeys?: any[], - tableCheck?: string): Promise { + createTable(name: string, columns: SQLiteDBColumnSchema[], primaryKeys?: string[], uniqueKeys?: string[][], + foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string): Promise { const sql = this.buildCreateTableSql(name, columns, primaryKeys, uniqueKeys, foreignKeys, tableCheck); return this.execute(sql); @@ -236,10 +335,10 @@ export class SQLiteDB { /** * Create a table if it doesn't exist from a schema. * - * @param {any} table Table schema. + * @param {SQLiteDBTableSchema} table Table schema. * @return {Promise} Promise resolved when success. */ - createTableFromSchema(table: any): Promise { + createTableFromSchema(table: SQLiteDBTableSchema): Promise { return this.createTable(table.name, table.columns, table.primaryKeys, table.uniqueKeys, table.foreignKeys, table.tableCheck); } @@ -247,10 +346,10 @@ export class SQLiteDB { /** * Create several tables if they don't exist from a list of schemas. * - * @param {any[]} tables List of table schema. + * @param {SQLiteDBTableSchema[]} tables List of table schema. * @return {Promise} Promise resolved when success. */ - createTablesFromSchema(tables: any[]): Promise { + createTablesFromSchema(tables: SQLiteDBTableSchema[]): Promise { const promises = []; tables.forEach((table) => { promises.push(this.createTableFromSchema(table)); @@ -308,6 +407,16 @@ export class SQLiteDB { return this.execute(`DELETE FROM ${table} ${select}`, params); } + /** + * Drop a table if it exists. + * + * @param {string} name The table name. + * @return {Promise} Promise resolved when success. + */ + dropTable(name: string): Promise { + return this.execute(`DROP TABLE IF EXISTS ${name}`); + } + /** * Execute a SQL query. * IMPORTANT: Use this function only if you cannot use any of the other functions in this API. Please take into account that @@ -684,6 +793,23 @@ export class SQLiteDB { return this.executeBatch(statements); } + /** + * Insert multiple records into database from another table. + * + * @param {string} table The database table to be inserted into. + * @param {string} source The database table to get the records from. + * @param {object} [conditions] The conditions to build the where clause. Must not contain numeric indexes. + * @param {string} [fields='*'] A comma separated list of fields to return. + * @return {Promise} Promise resolved when done. + */ + insertRecordsFrom(table: string, source: string, conditions?: object, fields: string = '*'): Promise { + const selectAndParams = this.whereClause(conditions); + const select = selectAndParams[0] ? 'WHERE ' + selectAndParams[0] : ''; + const params = selectAndParams[1]; + + return this.execute(`INSERT INTO ${table} SELECT ${fields} FROM ${source} ${select}`, params); + } + /** * Ensures that limit params are numeric and positive integers, to be passed to the database. * We explicitly treat null, '' and -1 as 0 in order to provide compatibility with how limit @@ -776,6 +902,16 @@ export class SQLiteDB { }); } + /** + * Test whether a table exists.. + * + * @param {string} name The table name. + * @return {Promise} Promise resolved if exists, rejected otherwise. + */ + tableExists(name: string): Promise { + return this.recordExists('sqlite_master', {type: 'table', tbl_name: name}); + } + /** * Update one or more records in a table. * diff --git a/src/components/attachments/attachments.ts b/src/components/attachments/attachments.ts index a44eb6bb3..03ada9d5d 100644 --- a/src/components/attachments/attachments.ts +++ b/src/components/attachments/attachments.ts @@ -77,7 +77,9 @@ export class CoreAttachmentsComponent implements OnInit { this.maxSubmissionsReadable = String(this.maxSubmissions); } - if (this.acceptedTypes && this.acceptedTypes.trim()) { + this.acceptedTypes = this.acceptedTypes && this.acceptedTypes.trim(); + + if (this.acceptedTypes && this.acceptedTypes != '*') { this.fileTypes = this.fileUploaderProvider.prepareFiletypeList(this.acceptedTypes); } } diff --git a/src/components/chrono/chrono.ts b/src/components/chrono/chrono.ts index 5770db45f..8783416af 100644 --- a/src/components/chrono/chrono.ts +++ b/src/components/chrono/chrono.ts @@ -28,7 +28,7 @@ import { Component, Input, OnChanges, OnDestroy, Output, EventEmitter, SimpleCha */ @Component({ selector: 'core-chrono', - template: '{{ time / 1000 | coreSecondsToHMS }}' + template: '{{ time / 1000 | coreSecondsToHMS }}' }) export class CoreChronoComponent implements OnChanges, OnDestroy { @Input() running: boolean; // Set it to true to start the chrono. Set it to false to stop it. diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 0e631c2b9..e72475548 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -51,6 +51,7 @@ import { CoreIonTabsComponent } from './ion-tabs/ion-tabs'; import { CoreIonTabComponent } from './ion-tabs/ion-tab'; import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading'; import { CoreUserAvatarComponent } from './user-avatar/user-avatar'; +import { CoreStyleComponent } from './style/style'; @NgModule({ declarations: [ @@ -87,7 +88,8 @@ import { CoreUserAvatarComponent } from './user-avatar/user-avatar'; CoreIonTabsComponent, CoreIonTabComponent, CoreInfiniteLoadingComponent, - CoreUserAvatarComponent + CoreUserAvatarComponent, + CoreStyleComponent ], entryComponents: [ CoreContextMenuPopoverComponent, @@ -131,7 +133,8 @@ import { CoreUserAvatarComponent } from './user-avatar/user-avatar'; CoreIonTabsComponent, CoreIonTabComponent, CoreInfiniteLoadingComponent, - CoreUserAvatarComponent + CoreUserAvatarComponent, + CoreStyleComponent ] }) export class CoreComponentsModule {} diff --git a/src/components/context-menu/context-menu-popover.ts b/src/components/context-menu/context-menu-popover.ts index 7aa987c24..aa468f189 100644 --- a/src/components/context-menu/context-menu-popover.ts +++ b/src/components/context-menu/context-menu-popover.ts @@ -26,12 +26,14 @@ import { CoreLoggerProvider } from '@providers/logger'; }) export class CoreContextMenuPopoverComponent { title: string; + uniqueId: string; items: CoreContextMenuItemComponent[]; protected logger: any; constructor(navParams: NavParams, private viewCtrl: ViewController, logger: CoreLoggerProvider) { this.title = navParams.get('title'); this.items = navParams.get('items') || []; + this.uniqueId = navParams.get('id'); this.logger = logger.getInstance('CoreContextMenuPopoverComponent'); } diff --git a/src/components/context-menu/context-menu.ts b/src/components/context-menu/context-menu.ts index 5d453384f..1f674eebd 100644 --- a/src/components/context-menu/context-menu.ts +++ b/src/components/context-menu/context-menu.ts @@ -16,6 +16,7 @@ import { Component, Input, OnInit, OnDestroy, ElementRef, Optional } from '@angu import { PopoverController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreContextMenuItemComponent } from './context-menu-item'; import { CoreContextMenuPopoverComponent } from './context-menu-popover'; import { CoreTabComponent } from '@components/tabs/tab'; @@ -34,14 +35,16 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { hideMenu = true; // It will be unhidden when items are added. ariaLabel: string; + expanded = false; protected items: CoreContextMenuItemComponent[] = []; protected itemsMovedToParent: CoreContextMenuItemComponent[] = []; protected itemsChangedStream: Subject; // Stream to update the hideMenu boolean when items change. protected instanceId: string; protected parentContextMenu: CoreContextMenuComponent; + protected uniqueId: string; constructor(private translate: TranslateService, private popoverCtrl: PopoverController, elementRef: ElementRef, - private domUtils: CoreDomUtilsProvider, @Optional() public coreTab: CoreTabComponent) { + private domUtils: CoreDomUtilsProvider, @Optional() public coreTab: CoreTabComponent, utils: CoreUtilsProvider) { // Create the stream and subscribe to it. We ignore successive changes during 250ms. this.itemsChangedStream = new Subject(); this.itemsChangedStream.auditTime(250).subscribe(() => { @@ -56,6 +59,9 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { }); }); + // Calculate the unique ID. + this.uniqueId = 'core-context-menu-' + utils.getUniqueId('CoreContextMenuComponent'); + this.instanceId = this.domUtils.storeInstanceByElement(elementRef.nativeElement, this); } @@ -170,10 +176,19 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { * @param {MouseEvent} event Event. */ showContextMenu(event: MouseEvent): void { - const popover = this.popoverCtrl.create(CoreContextMenuPopoverComponent, { title: this.title, items: this.items }); - popover.present({ - ev: event - }); + if (!this.expanded) { + const popover = this.popoverCtrl.create(CoreContextMenuPopoverComponent, + { title: this.title, items: this.items, id: this.uniqueId }); + + popover.onDidDismiss(() => { + this.expanded = false; + }); + popover.present({ + ev: event + }); + + this.expanded = true; + } } /** diff --git a/src/components/context-menu/core-context-menu-popover.html b/src/components/context-menu/core-context-menu-popover.html index 81a1965fb..18905549d 100644 --- a/src/components/context-menu/core-context-menu-popover.html +++ b/src/components/context-menu/core-context-menu-popover.html @@ -1,7 +1,7 @@ - + {{title}} - - + + diff --git a/src/components/context-menu/core-context-menu.html b/src/components/context-menu/core-context-menu.html index 262bb3256..0628573be 100644 --- a/src/components/context-menu/core-context-menu.html +++ b/src/components/context-menu/core-context-menu.html @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/src/components/empty-box/empty-box.scss b/src/components/empty-box/empty-box.scss index 4eb024d88..5d265575c 100644 --- a/src/components/empty-box/empty-box.scss +++ b/src/components/empty-box/empty-box.scss @@ -29,6 +29,7 @@ ion-app.app-root core-empty-box { .icon { font-size: 120px; + width: auto; } img { height: 125px; diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts index 8171e018f..0321a6485 100644 --- a/src/components/icon/icon.ts +++ b/src/components/icon/icon.ts @@ -37,6 +37,8 @@ export class CoreIconComponent implements OnInit, OnDestroy { // FontAwesome params. @Input('fixed-width') fixedWidth: string; + @Input('label') ariaLabel?: string; + protected element: HTMLElement; protected newElement: HTMLElement; @@ -64,6 +66,10 @@ export class CoreIconComponent implements OnInit, OnDestroy { this.newElement = this.element.firstElementChild; } + !this.ariaLabel && this.newElement.setAttribute('aria-hidden', 'true'); + !this.ariaLabel && this.newElement.setAttribute('role', 'presentation'); + this.ariaLabel && this.newElement.setAttribute('aria-label', this.ariaLabel); + const attrs = this.element.attributes; for (let i = attrs.length - 1; i >= 0; i--) { if (attrs[i].name == 'class') { diff --git a/src/components/infinite-loading/infinite-loading.ts b/src/components/infinite-loading/infinite-loading.ts index 6b72837f0..101e303d4 100644 --- a/src/components/infinite-loading/infinite-loading.ts +++ b/src/components/infinite-loading/infinite-loading.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { InfiniteScroll } from 'ionic-angular'; +import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; +import { InfiniteScroll, Content } from 'ionic-angular'; /** * Component to show a infinite loading trigger and spinner while more data is being loaded. @@ -25,7 +25,7 @@ import { InfiniteScroll } from 'ionic-angular'; selector: 'core-infinite-loading', templateUrl: 'core-infinite-loading.html', }) -export class CoreInfiniteLoadingComponent { +export class CoreInfiniteLoadingComponent implements OnChanges { @Input() enabled: boolean; @Input() error = false; @Input() position = 'bottom'; @@ -35,10 +35,26 @@ export class CoreInfiniteLoadingComponent { protected infiniteScroll: InfiniteScroll; - constructor() { + constructor(@Optional() private content: Content) { this.action = new EventEmitter(); } + /** + * Detect changes on input properties. + * + * @param {SimpleChange}} changes Changes. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.enabled && this.enabled && this.position == 'bottom') { + // Infinite scroll enabled. If the list doesn't fill the full height, infinite scroll isn't triggered automatically. + // Send a fake scroll event to make infinite scroll check if it should load more items. + setTimeout(() => { + const event: any = new Event('scroll'); + this.content.ionScroll.emit(event); + }, 400); + } + } + /** * Load More items calling the action provided. * @@ -64,6 +80,13 @@ export class CoreInfiniteLoadingComponent { this.loadingMore = false; this.infiniteScroll && this.infiniteScroll.complete(); this.infiniteScroll = undefined; + + // More items loaded. If the list doesn't fill the full height, infinite scroll isn't triggered automatically. + // Send a fake scroll event to make infinite scroll check if it should load more items. + setTimeout(() => { + const event: any = new Event('scroll'); + this.content.ionScroll.emit(event); + }); } } diff --git a/src/components/input-errors/core-input-errors.html b/src/components/input-errors/core-input-errors.html index e960807e8..62ab3df6a 100644 --- a/src/components/input-errors/core-input-errors.html +++ b/src/components/input-errors/core-input-errors.html @@ -12,5 +12,5 @@
-
{{ errorText }}
+
{{ errorText }}
\ No newline at end of file diff --git a/src/components/ion-tabs/core-ion-tabs.html b/src/components/ion-tabs/core-ion-tabs.html index 8a94426df..e255871b6 100644 --- a/src/components/ion-tabs/core-ion-tabs.html +++ b/src/components/ion-tabs/core-ion-tabs.html @@ -1,5 +1,5 @@ -
- +
+
diff --git a/src/components/ion-tabs/ion-tabs.ts b/src/components/ion-tabs/ion-tabs.ts index 4a6eaccf8..deeb76a53 100644 --- a/src/components/ion-tabs/ion-tabs.ts +++ b/src/components/ion-tabs/ion-tabs.ts @@ -53,6 +53,7 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy { @ViewChild('originalTabs') originalTabsRef: ElementRef; _loaded: boolean; // Whether tabs have been loaded. + hidden = false; // Whether to show/hide tabs. /** * List of tabs that haven't been initialized yet. This is required because IonTab calls add() on the constructor, @@ -325,6 +326,22 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy { } } + /** + * Change tabs visibility to show/hide them from the view. + * + * @param {boolean} visible If show or hide the tabs. + */ + changeVisibility(visible: boolean): void { + if (this.hidden == visible) { + // Change needed. + this.hidden = !visible; + + setTimeout(() => { + this.viewCtrl.getContent().resize(); + }); + } + } + /** * Component destroyed. */ diff --git a/src/components/loading/core-loading.html b/src/components/loading/core-loading.html index 0e34fe572..c149e9bf1 100644 --- a/src/components/loading/core-loading.html +++ b/src/components/loading/core-loading.html @@ -1,10 +1,10 @@ -
+
-

{{message}}

+

{{message}}

-
+
\ No newline at end of file diff --git a/src/components/local-file/local-file.ts b/src/components/local-file/local-file.ts index f507b2275..b5f3140ec 100644 --- a/src/components/local-file/local-file.ts +++ b/src/components/local-file/local-file.ts @@ -62,13 +62,6 @@ export class CoreLocalFileComponent implements OnInit { ngOnInit(): void { this.manage = this.utils.isTrueOrOne(this.manage); - // Let's calculate the relative path for the file. - this.relativePath = this.fileProvider.removeBasePath(this.file.toURL()); - if (!this.relativePath) { - // Didn't find basePath, use fullPath but if the user tries to manage the file it'll probably fail. - this.relativePath = this.file.fullPath; - } - this.loadFileBasicData(); // Get the size and timemodified. @@ -88,6 +81,13 @@ export class CoreLocalFileComponent implements OnInit { this.fileName = this.file.name; this.fileIcon = this.mimeUtils.getFileIcon(this.file.name); this.fileExtension = this.mimeUtils.getFileExtension(this.file.name); + + // Let's calculate the relative path for the file. + this.relativePath = this.fileProvider.removeBasePath(this.file.toURL()); + if (!this.relativePath) { + // Didn't find basePath, use fullPath but if the user tries to manage the file it'll probably fail. + this.relativePath = this.file.fullPath; + } } /** diff --git a/src/components/progress-bar/core-progress-bar.html b/src/components/progress-bar/core-progress-bar.html index 7c7319034..d024ba4b8 100644 --- a/src/components/progress-bar/core-progress-bar.html +++ b/src/components/progress-bar/core-progress-bar.html @@ -1,5 +1,5 @@
- +
diff --git a/src/components/recaptcha/core-recaptcha.html b/src/components/recaptcha/core-recaptcha.html index a0688a12b..d4e096dba 100644 --- a/src/components/recaptcha/core-recaptcha.html +++ b/src/components/recaptcha/core-recaptcha.html @@ -2,7 +2,7 @@
- +

{{ 'core.answered' | translate }}

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

diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html index 972fe0c0c..7a02610dd 100644 --- a/src/components/rich-text-editor/core-rich-text-editor.html +++ b/src/components/rich-text-editor/core-rich-text-editor.html @@ -1,5 +1,5 @@
-
+
@@ -22,7 +22,7 @@
- +
diff --git a/src/components/search-box/core-search-box.html b/src/components/search-box/core-search-box.html index 7712caf71..260d45215 100644 --- a/src/components/search-box/core-search-box.html +++ b/src/components/search-box/core-search-box.html @@ -1,7 +1,7 @@ -
+ - + diff --git a/src/components/send-message-form/core-send-message-form.html b/src/components/send-message-form/core-send-message-form.html index aea20f503..b1829854a 100644 --- a/src/components/send-message-form/core-send-message-form.html +++ b/src/components/send-message-form/core-send-message-form.html @@ -1,5 +1,5 @@ - +