diff --git a/package-lock.json b/package-lock.json index 11a5ea75b..1c2e18d0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,46 +2,128 @@ "name": "moodlemobile", "version": "3.5.0", "lockfileVersion": 1, + "requires": true, "dependencies": { + "7zip-bin": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-2.2.7.tgz", + "integrity": "sha1-ckgCuNa9oL8s/mGkuGqCDvyOzpM=", + "optional": true, + "requires": { + "7zip-bin-linux": "1.3.1", + "7zip-bin-mac": "1.0.1", + "7zip-bin-win": "2.2.0" + } + }, + "7zip-bin-linux": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/7zip-bin-linux/-/7zip-bin-linux-1.3.1.tgz", + "integrity": "sha512-Wv1uEEeHbTiS1+ycpwUxYNuIcyohU6Y6vEqY3NquBkeqy0YhVdsNUGsj0XKSRciHR6LoJSEUuqYUexmws3zH7Q==", + "optional": true + }, + "7zip-bin-mac": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/7zip-bin-mac/-/7zip-bin-mac-1.0.1.tgz", + "integrity": "sha1-Pmh3i78JJq3GgVlCcHRQXUdVXAI=", + "optional": true + }, + "7zip-bin-win": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin-win/-/7zip-bin-win-2.2.0.tgz", + "integrity": "sha512-uPHXapEmUtlUKTBx4asWMlxtFUWXzEY0KVEgU7QKhgO2LJzzM3kYxM6yOyUZTtYE6mhK4dDn3FDut9SCQWHzgg==", + "optional": true + }, + "@angular-devkit/build-optimizer": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.0.35.tgz", + "integrity": "sha512-7JxZZAYFSCc0tP6+NrRn3b2Cd1b9d+a3+OfwVNyNsNd2unelqUMko2hm0KLbC8BXcXt/OILg1E/ZgLAXSS47nw==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "source-map": "0.5.7", + "typescript": "2.4.2", + "webpack-sources": "1.1.0" + } + }, + "@angular/animations": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.9.tgz", + "integrity": "sha1-Ig25+1pSoZPbACPXIbI93SWnV3A=", + "requires": { + "tslib": "1.8.1" + } + }, "@angular/common": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.0.0.tgz", - "integrity": "sha1-+W1mpRe5ldG6mygwnxXC41lnWCU=" + "integrity": "sha1-+W1mpRe5ldG6mygwnxXC41lnWCU=", + "requires": { + "tslib": "1.8.1" + } }, "@angular/compiler": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.0.0.tgz", - "integrity": "sha1-uf+/GMijnYt9rOxHMZOpDiTMK8k=" + "integrity": "sha1-uf+/GMijnYt9rOxHMZOpDiTMK8k=", + "requires": { + "tslib": "1.8.1" + } }, "@angular/compiler-cli": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-5.0.0.tgz", - "integrity": "sha1-Dsu5N9hKT43ZTwwqR7B9LkaUyFM=" + "integrity": "sha1-Dsu5N9hKT43ZTwwqR7B9LkaUyFM=", + "requires": { + "chokidar": "1.7.0", + "minimist": "1.2.0", + "reflect-metadata": "0.1.10", + "tsickle": "0.24.1" + } }, "@angular/core": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.0.0.tgz", - "integrity": "sha1-T5dqIl993fNJkvLK2CTJVDpG9Mg=" + "integrity": "sha1-T5dqIl993fNJkvLK2CTJVDpG9Mg=", + "requires": { + "tslib": "1.8.1" + } }, "@angular/forms": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.0.0.tgz", - "integrity": "sha1-x/3fo1OWdZrphSkgowzdqMQe0d4=" + "integrity": "sha1-x/3fo1OWdZrphSkgowzdqMQe0d4=", + "requires": { + "tslib": "1.8.1" + } }, "@angular/http": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.0.0.tgz", - "integrity": "sha1-Byiivgz7sHhyfF64fUyF1T/smlE=" + "integrity": "sha1-Byiivgz7sHhyfF64fUyF1T/smlE=", + "requires": { + "tslib": "1.8.1" + } }, "@angular/platform-browser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.0.0.tgz", - "integrity": "sha1-xwOPfN6AcFtiAUiXIx4YLuyXb+0=" + "integrity": "sha1-xwOPfN6AcFtiAUiXIx4YLuyXb+0=", + "requires": { + "tslib": "1.8.1" + } }, "@angular/platform-browser-dynamic": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.0.0.tgz", - "integrity": "sha1-iH4QbIsQOwQVz2FWpCXabYP0yJ0=" + "integrity": "sha1-iH4QbIsQOwQVz2FWpCXabYP0yJ0=", + "requires": { + "tslib": "1.8.1" + } + }, + "@ionic-native/badge": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@ionic-native/badge/-/badge-4.6.0.tgz", + "integrity": "sha1-/L4+HDpd9skKhtC2LZzBJ2so0JA=" }, "@ionic-native/camera": { "version": "4.5.2", @@ -56,7 +138,12 @@ "@ionic-native/core": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-4.3.0.tgz", - "integrity": "sha512-Pf0qCzqlVFmIpZpvo35Kl0e+1K8GUgPMcKBnN57gWh+5Ecj3dPcb+MbP4murJo/dnFsIYPYdXRZRf74hjo6gtw==" + "integrity": "sha1-DWE7tsh3kUaUlRQpnKHAJhHjCKk=" + }, + "@ionic-native/device": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@ionic-native/device/-/device-4.6.0.tgz", + "integrity": "sha1-NvvxsjbfFqmNgKxz9gzJsp6aHps=" }, "@ionic-native/file": { "version": "4.5.2", @@ -98,10 +185,15 @@ "resolved": "https://registry.npmjs.org/@ionic-native/network/-/network-4.5.2.tgz", "integrity": "sha512-4tChqfuTLe7TJdKMyk0l5mZn6Fyvg8PyI8WvNaFtrTkRN/hFT6LaC6Qe5rSH2WFRzGxuYWZjh+uhCG6KOvwOCg==" }, + "@ionic-native/push": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@ionic-native/push/-/push-4.6.0.tgz", + "integrity": "sha1-TeQ/Wl0rTW/ARaEvTtJjs1240GM=" + }, "@ionic-native/splash-screen": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@ionic-native/splash-screen/-/splash-screen-4.3.0.tgz", - "integrity": "sha512-psShN4u3hiLkuva8GaCUqwFuVrd8nizXxM8n0eZyFdXedFHFF3CqBMPzPdH98fcoseyOD7QekOtrm0lV6QlD8w==" + "integrity": "sha1-nesHr88QWYByM6p9kNbkQfbss3I=" }, "@ionic-native/sqlite": { "version": "4.5.2", @@ -111,7 +203,7 @@ "@ionic-native/status-bar": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@ionic-native/status-bar/-/status-bar-4.3.0.tgz", - "integrity": "sha512-gjS0U2uT6XYshysvzNu98Pf6b5SZ7SGSYkZW1mft19geFn6/MKunX1CJkjpXmiTn14nAD1+FBxF43Oi2OfoM4g==" + "integrity": "sha1-3bFoYaIscjXAc3uk4OUSfJruSs4=" }, "@ionic-native/zip": { "version": "4.5.2", @@ -119,147 +211,37 @@ "integrity": "sha512-GIjqQL+MsKphR3Ye0CsTH7ztTMLebc0ctbJR8mvMZA43vuy/shaefV3x7jHS9i4KiaLWCB0BqUL8HcPOGJL4HA==" }, "@ionic/app-scripts": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@ionic/app-scripts/-/app-scripts-3.1.5.tgz", - "integrity": "sha512-eQTcdkduGjz84s7gbdIZg2yq1pK8ZmCxQJ3IUYvjInuPeUbbk/lgRJMwryTd3f/xZVtHzFHv09hxIkBoxBs+Vw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@ionic/app-scripts/-/app-scripts-3.1.8.tgz", + "integrity": "sha512-5ZEdh4/rGJMAch+6vO7muNEAhIqfavjTea15WbUJtKkcMwR9qjbCMxBe5qRWCxKafhEqDZX2gA8/zwbjbhb+Zw==", "dev": true, - "dependencies": { - "@angular-devkit/build-optimizer": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.0.35.tgz", - "integrity": "sha512-7JxZZAYFSCc0tP6+NrRn3b2Cd1b9d+a3+OfwVNyNsNd2unelqUMko2hm0KLbC8BXcXt/OILg1E/ZgLAXSS47nw==", - "dev": true, - "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 - } - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true - }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "dev": true - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "typescript": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", - "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", - "dev": true - }, - "webpack": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz", - "integrity": "sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==", - "dev": true, - "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 - } - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "yargs": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", - "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", - "dev": true - }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "dev": true - } + "requires": { + "@angular-devkit/build-optimizer": "0.0.35", + "autoprefixer": "7.2.6", + "chalk": "2.3.2", + "chokidar": "1.7.0", + "clean-css": "4.1.11", + "cross-spawn": "5.1.0", + "express": "4.16.3", + "fs-extra": "4.0.3", + "glob": "7.1.2", + "json-loader": "0.5.7", + "node-sass": "4.7.2", + "os-name": "2.0.1", + "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.5.7", + "tiny-lr": "1.1.1", + "tslint": "5.9.1", + "tslint-eslint-rules": "4.1.1", + "uglify-es": "3.2.2", + "webpack": "3.8.1", + "ws": "3.3.2", + "xml2js": "0.4.19" } }, "@ngx-translate/core": { @@ -272,6 +254,91 @@ "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-2.0.0.tgz", "integrity": "sha1-nBbQfNBwxnraJwoulAKB64JrP0M=" }, + "@nodert-win10/windows.applicationmodel": { + "version": "0.2.96", + "resolved": "https://registry.npmjs.org/@nodert-win10/windows.applicationmodel/-/windows.applicationmodel-0.2.96.tgz", + "integrity": "sha1-v5Mh8xkB3kcPcWNKlZFbG1Afjr0=", + "optional": true, + "requires": { + "nan": "2.10.0" + }, + "dependencies": { + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + } + } + }, + "@nodert-win10/windows.data.xml.dom": { + "version": "0.2.96", + "resolved": "https://registry.npmjs.org/@nodert-win10/windows.data.xml.dom/-/windows.data.xml.dom-0.2.96.tgz", + "integrity": "sha1-RfH1BrY3X1hdrltdAsMNYBwC1FQ=", + "optional": true, + "requires": { + "nan": "2.10.0" + }, + "dependencies": { + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + } + } + }, + "@nodert-win10/windows.foundation": { + "version": "0.2.96", + "resolved": "https://registry.npmjs.org/@nodert-win10/windows.foundation/-/windows.foundation-0.2.96.tgz", + "integrity": "sha1-vrIGYdh/s1L5xcfEWJm/PCTXjNA=", + "optional": true, + "requires": { + "nan": "2.10.0" + }, + "dependencies": { + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + } + } + }, + "@nodert-win10/windows.ui.notifications": { + "version": "0.2.96", + "resolved": "https://registry.npmjs.org/@nodert-win10/windows.ui.notifications/-/windows.ui.notifications-0.2.96.tgz", + "integrity": "sha1-CwOeBqDARm7C6PJ6uHyxCQZL0LA=", + "optional": true, + "requires": { + "nan": "2.10.0" + }, + "dependencies": { + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + } + } + }, + "@nodert-win10/windows.ui.startscreen": { + "version": "0.2.96", + "resolved": "https://registry.npmjs.org/@nodert-win10/windows.ui.startscreen/-/windows.ui.startscreen-0.2.96.tgz", + "integrity": "sha1-qukSAniyE3Z7glNP5R6u+GRJHYo=", + "optional": true, + "requires": { + "nan": "2.10.0" + }, + "dependencies": { + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + } + } + }, "@types/cordova": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", @@ -285,7 +352,10 @@ "@types/cordova-plugin-file-transfer": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/cordova-plugin-file-transfer/-/cordova-plugin-file-transfer-0.0.3.tgz", - "integrity": "sha1-u6d+jQTQejlRR5eiA8JXxbECNoA=" + "integrity": "sha1-u6d+jQTQejlRR5eiA8JXxbECNoA=", + "requires": { + "@types/cordova-plugin-file": "0.0.3" + } }, "@types/cordova-plugin-globalization": { "version": "0.0.3", @@ -305,24 +375,28 @@ "@types/promise.prototype.finally": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/promise.prototype.finally/-/promise.prototype.finally-2.0.2.tgz", - "integrity": "sha512-Fs99h+iFQZ4ZY2vO3+uJCrx+5KQnJ4FPerZ3oT/1L5aA7vnmK/d7Z/Ml1yHtNCh9UQcjFTR4Xo/Jss2f39Fgtw==" + "integrity": "sha1-aP77jw4j8hiTozdAyTHHn0S+NJk=" }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=", "dev": true }, "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", - "dev": true + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "dev": true, + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } }, "acorn": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", - "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", + "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", "dev": true }, "acorn-dynamic-import": { @@ -330,6 +404,9 @@ "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", "dev": true, + "requires": { + "acorn": "4.0.13" + }, "dependencies": { "acorn": { "version": "4.0.13", @@ -340,10 +417,16 @@ } }, "ajv": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", - "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", - "dev": true + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } }, "ajv-keywords": { "version": "2.1.1", @@ -355,19 +438,21 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } }, "ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "dev": true + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } }, "ansi-regex": { "version": "2.1.1", @@ -376,10 +461,12 @@ "dev": true }, "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.1" + } }, "ansi-wrap": { "version": "0.1.0", @@ -390,14 +477,46 @@ "anymatch": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==" + "integrity": "sha1-VT3Lj5HjyImEXf26NMd3IbkLnXo=", + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=", "dev": true }, + "archiver": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz", + "integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=", + "requires": { + "archiver-utils": "1.3.0", + "async": "2.6.0", + "buffer-crc32": "0.2.13", + "glob": "7.1.2", + "lodash": "4.17.5", + "readable-stream": "2.3.3", + "tar-stream": "1.5.5", + "zip-stream": "1.2.0" + } + }, + "archiver-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", + "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", + "requires": { + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "lazystream": "1.0.0", + "lodash": "4.17.5", + "normalize-path": "2.1.1", + "readable-stream": "2.3.3" + } + }, "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", @@ -408,17 +527,32 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "dev": true + "dev": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "1.0.3" + } }, "arr-diff": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=" + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "1.1.0" + } }, "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=" }, "array-differ": { "version": "1.0.0", @@ -447,7 +581,7 @@ "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "integrity": "sha1-42jqFfibxwaff/uJrsOmx9SsItQ=", "dev": true }, "array-uniq": { @@ -468,34 +602,49 @@ "dev": true }, "asn1.js": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.2.tgz", - "integrity": "sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==", - "dev": true + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } }, "assert": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "dev": true + "dev": true, + "requires": { + "util": "0.10.3" + } }, "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", "dev": true }, "async": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "dev": true + "integrity": "sha1-YaKau2/MAm/qd+VtHG7FOnlZUfQ=", + "requires": { + "lodash": "4.17.5" + } }, "async-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" }, + "async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha1-i9iwJLDsmxwBzMua+dspvXF9+vM=" + }, "async-foreach": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", @@ -505,7 +654,7 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=", "dev": true }, "asynckit": { @@ -515,21 +664,29 @@ "dev": true }, "autoprefixer": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.2.tgz", - "integrity": "sha512-eTVoSHiGp2cDytg7RS7gtqAnfH+WFcNQMTjywGNu+hH7ViQZ/ZKsvNz2C1oVhCtd9DjMIC15iatpxmtp5Kxvpg==", - "dev": true + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.6.tgz", + "integrity": "sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ==", + "dev": true, + "requires": { + "browserslist": "2.11.3", + "caniuse-lite": "1.0.30000824", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "6.0.21", + "postcss-value-parser": "3.3.0" + } }, "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", "dev": true }, "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", "dev": true }, "babel-code-frame": { @@ -537,6 +694,11 @@ "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, "dependencies": { "ansi-styles": { "version": "2.2.1", @@ -548,7 +710,14 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } }, "supports-color": { "version": "2.0.0", @@ -564,9 +733,9 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base64-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", - "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz", + "integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w==", "dev": true }, "bcrypt-pbkdf": { @@ -574,7 +743,10 @@ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", "dev": true, - "optional": true + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } }, "beeper": { "version": "1.1.1", @@ -585,7 +757,7 @@ "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==", + "integrity": "sha1-pfwpi4G54Nyi5FiCR4S2XFK6WI4=", "dev": true }, "binary-extensions": { @@ -593,16 +765,70 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "2.3.6", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "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.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk=" + }, + "bluebird-lst": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz", + "integrity": "sha1-vryDAmt+kqcocaPcWZ4hnL+wAqk=", + "requires": { + "bluebird": "3.5.1" + } }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8=", "dev": true }, "body": { @@ -610,6 +836,12 @@ "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", "dev": true, + "requires": { + "continuable-cache": "0.3.1", + "error": "7.0.2", + "raw-body": "1.1.7", + "safe-json-parse": "1.0.1" + }, "dependencies": { "bytes": { "version": "1.0.0", @@ -621,13 +853,11 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true + "dev": true, + "requires": { + "bytes": "1.0.0", + "string_decoder": "0.10.31" + } } } }, @@ -636,30 +866,46 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", "dev": true, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true - } + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.3", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.16" } }, "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "dev": true + "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.16.3" + } }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=" + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } }, "braces": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=" + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } }, "brorand": { "version": "1.1.0", @@ -672,62 +918,105 @@ "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", "dev": true, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } + "requires": { + "resolve": "1.5.0" } }, "browserify-aes": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", - "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } }, "browserify-cipher": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", - "dev": true + "dev": true, + "requires": { + "browserify-aes": "1.2.0", + "browserify-des": "1.0.0", + "evp_bytestokey": "1.0.3" + } }, "browserify-des": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", - "dev": true + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + } }, "browserify-rsa": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true + "dev": true, + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.6" + } }, "browserify-sign": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.0" + } }, "browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true + "integrity": "sha1-KGlFnZqjviRf6P4sofRuLn9U1z8=", + "dev": true, + "requires": { + "pako": "1.0.6" + } }, "browserslist": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.10.0.tgz", - "integrity": "sha512-WyvzSLsuAVPOjbljXnyeWl14Ae+ukAT8MUuagKVzIDvwBxl4UAwD1xqtyQs2eWYPGUKMeC3Ol62goqYuKqTTcw==", - "dev": true + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "1.0.30000824", + "electron-to-chromium": "1.3.42" + } }, "buffer": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true + "dev": true, + "requires": { + "base64-js": "1.2.3", + "ieee754": "1.1.11", + "isarray": "1.0.0" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, "buffer-xor": { "version": "1.0.3", @@ -735,6 +1024,85 @@ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, + "builder-util": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-4.2.5.tgz", + "integrity": "sha1-urwZDi8sNoFJdjK1zCdPFUOqkmQ=", + "requires": { + "7zip-bin": "3.0.0", + "bluebird-lst": "1.0.5", + "builder-util-runtime": "4.2.0", + "chalk": "2.3.2", + "debug": "3.1.0", + "fs-extra-p": "4.5.2", + "ini": "1.3.5", + "is-ci": "1.1.0", + "js-yaml": "3.11.0", + "lazy-val": "1.0.3", + "semver": "5.5.0", + "source-map-support": "0.5.4", + "stat-mode": "0.2.2", + "temp-file": "3.1.1", + "tunnel-agent": "0.6.0" + }, + "dependencies": { + "7zip-bin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-3.0.0.tgz", + "integrity": "sha1-F0FtxUL0FRGyapZnuShH117xUP4=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.4.tgz", + "integrity": "sha512-PETSPG6BjY1AHs2t64vS2aqAgu6dMIMXJULWFBGbh2Gr8nVLbCFDo6i/RMMvviIQ2h1Z8+5gQhVKSn2je9nmdg==", + "requires": { + "source-map": "0.6.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "builder-util-runtime": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.2.0.tgz", + "integrity": "sha512-cROCExnJOJvRD58HHcnrrgyRAoDHGZT0hKox0op7vTuuuRC/1JKMXvSR+Hxy7KWy/aEmKu0HfSqMd4znDEqQsA==", + "requires": { + "bluebird-lst": "1.0.5", + "debug": "3.1.0", + "fs-extra-p": "4.5.2", + "sax": "1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -763,54 +1131,94 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } }, "caniuse-lite": { - "version": "1.0.30000783", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000783.tgz", - "integrity": "sha1-m1SZ+xtQPSNF0SqmuGEoUvQnb/0=", + "version": "1.0.30000824", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000824.tgz", + "integrity": "sha512-KcgeAvVkpzN05Mjiyz5vf0le5AWRwfRGqGkKXWWsdrLQd4EIBevReSy7mYCdwSq7MqKrmJ0lEQEkUQE2VspRRw==", "dev": true }, "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", "dev": true }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } }, "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=" + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.1.3", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "ci-info": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", + "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==" }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true + "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } }, "clean-css": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.9.tgz", - "integrity": "sha1-Nc7ornaHpJuYA09w3gDE7dOCYwE=", - "dev": true + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } }, "clone": { "version": "1.0.3", @@ -839,27 +1247,42 @@ "color-convert": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "dev": true + "integrity": "sha1-wSYRB66y8pTr/+ye2eytUppgl+0=", + "requires": { + "color-name": "1.1.3" + } }, "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } }, "commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", "dev": true }, + "compress-commons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", + "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", + "requires": { + "buffer-crc32": "0.2.13", + "crc32-stream": "2.0.0", + "normalize-path": "2.1.1", + "readable-stream": "2.3.3" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -869,7 +1292,10 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true + "dev": true, + "requires": { + "date-now": "0.1.4" + } }, "console-control-strings": { "version": "1.1.0", @@ -892,7 +1318,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", "dev": true }, "continuable-cache": { @@ -923,67 +1349,129 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "crc": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz", + "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=" + }, + "crc32-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", + "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", + "requires": { + "crc": "3.5.0", + "readable-stream": "2.3.3" + } + }, "create-ecdh": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", - "dev": true + "dev": true, + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } }, "create-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", - "dev": true + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "sha.js": "2.4.11" + } }, "create-hmac": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", - "dev": true + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.11" + } }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true + "dev": true, + "requires": { + "lru-cache": "4.1.2", + "shebang-command": "1.2.0", + "which": "1.3.0" + } }, "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", "dev": true, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "dev": true - } + "requires": { + "boom": "2.10.1" } }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true + "integrity": "sha1-OWz58xN/A+S45TLFj2mCVOAPgOw=", + "dev": true, + "requires": { + "browserify-cipher": "1.0.0", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.0", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "diffie-hellman": "5.0.2", + "inherits": "2.0.3", + "pbkdf2": "3.0.14", + "public-encrypt": "4.0.0", + "randombytes": "2.0.6", + "randomfill": "1.0.4" + } }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true + "dev": true, + "requires": { + "es5-ext": "0.10.42" + } }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } }, "date-now": { "version": "0.1.4", @@ -997,6 +1485,14 @@ "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", "dev": true }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -1007,12 +1503,19 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "dev": true + "dev": true, + "requires": { + "clone": "1.0.3" + } }, "define-properties": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=" + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } }, "delayed-stream": { "version": "1.0.0", @@ -1027,9 +1530,9 @@ "dev": true }, "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", "dev": true }, "deprecated": { @@ -1042,7 +1545,11 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } }, "destroy": { "version": "1.0.4", @@ -1054,44 +1561,50 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", - "dev": true + "dev": true, + "requires": { + "fs-exists-sync": "0.1.0" + } }, "diff": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", - "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, "diffie-hellman": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", - "dev": true + "dev": true, + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.6" + } }, "doctrine": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", "dev": true, + "requires": { + "esutils": "1.1.6", + "isarray": "1.0.0" + }, "dependencies": { "esutils": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", "dev": true - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true } } }, "domain-browser": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", - "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, "duplexer2": { @@ -1099,6 +1612,9 @@ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", "dev": true, + "requires": { + "readable-stream": "1.1.14" + }, "dependencies": { "isarray": { "version": "0.0.1", @@ -1110,7 +1626,13 @@ "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } }, "string_decoder": { "version": "0.10.31", @@ -1125,7 +1647,10 @@ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", "dev": true, - "optional": true + "optional": true, + "requires": { + "jsbn": "0.1.1" + } }, "ee-first": { "version": "1.1.1", @@ -1133,23 +1658,56 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, - "electron-releases": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/electron-releases/-/electron-releases-2.1.0.tgz", - "integrity": "sha512-cyKFD1bTE/UgULXfaueIN1k5EPFzs+FRc/rvCY5tIynefAPqopQEgjr0EzY+U3Dqrk/G4m9tXSPuZ77v6dL/Rw==", - "dev": true + "electron-builder-squirrel-windows": { + "version": "19.56.0", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-19.56.0.tgz", + "integrity": "sha1-E5AVvk0CZ5WIJEbVxSbWv1JDkco=", + "requires": { + "7zip-bin": "2.2.7", + "archiver": "2.1.1", + "bluebird-lst": "1.0.5", + "builder-util": "4.2.5", + "fs-extra-p": "4.5.2", + "sanitize-filename": "1.6.1" + } }, "electron-to-chromium": { - "version": "1.3.30", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.30.tgz", - "integrity": "sha512-zx1Prv7kYLfc4OA60FhxGbSo4qrEjgSzpo1/37i7l9ltXPYOoQBtjQxY9KmsgfHnBxHlBGXwLlsbt/gub1w5lw==", + "version": "1.3.42", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz", + "integrity": "sha1-lcM78B0MxAVVauyJn+Yf1NduoPk=", "dev": true }, + "electron-windows-notifications": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/electron-windows-notifications/-/electron-windows-notifications-1.1.16.tgz", + "integrity": "sha1-zB8DnT2lBM0rwIkrSitg6FPi51M=", + "requires": { + "@nodert-win10/windows.applicationmodel": "0.2.96", + "@nodert-win10/windows.data.xml.dom": "0.2.96", + "@nodert-win10/windows.foundation": "0.2.96", + "@nodert-win10/windows.ui.notifications": "0.2.96", + "@nodert-win10/windows.ui.startscreen": "0.2.96", + "debug": "2.6.9", + "is-electron-renderer": "2.0.1", + "sanitize-xml-string": "1.1.0", + "uuid": "3.2.1", + "xml-escape": "1.1.0" + } + }, "elliptic": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", - "dev": true + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } }, "emojis-list": { "version": "2.1.0", @@ -1158,62 +1716,116 @@ "dev": true }, "encodeurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" + } + }, "enhanced-resolve": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", - "dev": true + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "object-assign": "4.1.1", + "tapable": "0.2.8" + } }, "errno": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.6.tgz", - "integrity": "sha512-IsORQDpaaSwcDP4ZZnHxgE85werpo34VYn1Ud3mq+eUsF593faR8oCZNXrROVkpFu2TsbrNhHin0aUrTsQ9vNw==", - "dev": true + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "1.0.1" + } }, "error": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", - "dev": true + "dev": true, + "requires": { + "string-template": "0.2.1", + "xtend": "4.0.1" + } }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "dev": true + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } }, "es-abstract": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", - "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==" + "integrity": "sha1-Hss2wZeEKgDY7kwt/YZGu5fWCGQ=", + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } }, "es-to-primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=" + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } }, "es5-ext": { - "version": "0.10.37", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz", - "integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=", - "dev": true + "version": "0.10.42", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz", + "integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==", + "dev": true, + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "next-tick": "1.0.0" + } }, "es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.42", + "es6-symbol": "3.1.1" + } }, "es6-map": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.42", + "es6-iterator": "2.0.3", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } }, "es6-promise": { "version": "3.0.2", @@ -1224,19 +1836,36 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.42", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } }, "es6-symbol": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.42" + } }, "es6-weak-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.42", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } }, "escape-html": { "version": "1.0.3", @@ -1247,20 +1876,33 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escope": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true + "dev": true, + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.1", + "estraverse": "4.2.0" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" }, "esrecurse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", - "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", - "dev": true + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } }, "estraverse": { "version": "4.2.0", @@ -1271,7 +1913,7 @@ "estree-walker": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.1.tgz", - "integrity": "sha512-7HgCgz1axW7w5aOvgOQkoR1RMBkllygJrssU3BvymKQ95lxXYv6Pon17fBRDm9qhkvXZGijOULoSF9ShOk/ZLg==", + "integrity": "sha1-ZPw3UFOrxvV9c+m9LwBGRK08WFQ=", "dev": true }, "esutils": { @@ -1290,7 +1932,11 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.42" + } }, "events": { "version": "1.1.1", @@ -1301,49 +1947,89 @@ "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true + "integrity": "sha1-f8vbGY3HGVlDLv4ThCaE4FJaywI=", + "dev": true, + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.1" + } }, "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } }, "expand-brackets": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=" + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "0.1.1" + } }, "expand-range": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=" + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "2.2.3" + } }, "expand-tilde": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", - "dev": true + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } }, "express": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", - "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", "dev": true, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - } + "requires": { + "accepts": "1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.2", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "1.7.0", + "proxy-addr": "2.0.3", + "qs": "6.5.1", + "range-parser": "1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "1.4.0", + "type-is": "1.6.16", + "utils-merge": "1.0.1", + "vary": "1.1.2" } }, "extend": { @@ -1355,7 +2041,10 @@ "extglob": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=" + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "1.0.0" + } }, "extsprintf": { "version": "1.3.0", @@ -1367,12 +2056,16 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.1.tgz", "integrity": "sha1-xKNGK6FK3137q3lzH9OESiBpy7s=", - "dev": true + "dev": true, + "requires": { + "ansi-gray": "0.1.1", + "time-stamp": "1.1.0" + } }, "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true }, "fast-json-stable-stringify": { @@ -1385,7 +2078,10 @@ "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "dev": true + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } }, "filename-regex": { "version": "2.0.1", @@ -1395,20 +2091,28 @@ "fill-range": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=" + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } }, "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true - } + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.4.0", + "unpipe": "1.0.0" } }, "find-index": { @@ -1421,25 +2125,45 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } }, "findup-sync": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz", "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", - "dev": true + "dev": true, + "requires": { + "detect-file": "0.1.0", + "is-glob": "2.0.1", + "micromatch": "2.3.11", + "resolve-dir": "0.1.1" + } }, "fined": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "is-plain-object": "2.0.4", + "object.defaults": "1.1.0", + "object.pick": "1.3.0", + "parse-filepath": "1.0.1" + }, "dependencies": { "expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1" + } } } }, @@ -1463,7 +2187,10 @@ "for-own": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=" + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "1.0.2" + } }, "foreach": { "version": "2.0.5", @@ -1477,10 +2204,15 @@ "dev": true }, "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "dev": true + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } }, "forwarded": { "version": "0.1.2", @@ -1500,17 +2232,52 @@ "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", "dev": true }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs-extra-p": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.5.2.tgz", + "integrity": "sha512-ZYqFpBdy9w7PsK+vB30j+TnHOyWHm/CJbUq1qqoE8tb71m6qgk5Wa7gp3MYQdlGFxb9vfznF+yD4jcl8l+y91A==", + "requires": { + "bluebird-lst": "1.0.5", + "fs-extra": "5.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", "optional": true, + "requires": { + "nan": "2.8.0", + "node-pre-gyp": "0.6.39" + }, "dependencies": { "abbrev": { "version": "1.1.0", @@ -1522,7 +2289,11 @@ "version": "4.11.8", "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "optional": true + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } }, "ansi-regex": { "version": "2.1.1", @@ -1539,7 +2310,11 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "optional": true + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } }, "asn1": { "version": "0.2.3", @@ -1580,22 +2355,35 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=" + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "2.0.3" + } }, "boom": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=" + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } }, "brace-expansion": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", - "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=" + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } }, "buffer-shims": { "version": "1.0.0", @@ -1622,7 +2410,10 @@ "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=" + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } }, "concat-map": { "version": "0.0.1", @@ -1642,13 +2433,19 @@ "cryptiles": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=" + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -1662,7 +2459,10 @@ "version": "2.6.8", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "optional": true + "optional": true, + "requires": { + "ms": "2.0.0" + } }, "deep-extend": { "version": "0.4.2", @@ -1691,7 +2491,10 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true + "optional": true, + "requires": { + "jsbn": "0.1.1" + } }, "extend": { "version": "3.0.1", @@ -1714,7 +2517,12 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "optional": true + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } }, "fs.realpath": { "version": "1.0.0", @@ -1724,25 +2532,49 @@ "fstream": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=" + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } }, "fstream-ignore": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "optional": true + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -1755,7 +2587,15 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==" + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } }, "graceful-fs": { "version": "4.1.11", @@ -1772,7 +2612,11 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "optional": true + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } }, "has-unicode": { "version": "2.0.1", @@ -1783,7 +2627,13 @@ "hawk": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=" + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } }, "hoek": { "version": "2.16.3", @@ -1794,12 +2644,21 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "optional": true + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } }, "inherits": { "version": "2.0.3", @@ -1815,7 +2674,10 @@ "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=" + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } }, "is-typedarray": { "version": "1.0.0", @@ -1838,7 +2700,10 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", - "optional": true + "optional": true, + "requires": { + "jsbn": "0.1.1" + } }, "jsbn": { "version": "0.1.1", @@ -1856,7 +2721,10 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "optional": true + "optional": true, + "requires": { + "jsonify": "0.0.0" + } }, "json-stringify-safe": { "version": "5.0.1", @@ -1875,6 +2743,12 @@ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -1892,12 +2766,18 @@ "mime-types": { "version": "2.1.15", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", - "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=" + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "requires": { + "mime-db": "1.27.0" + } }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==" + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.7" + } }, "minimist": { "version": "0.0.8", @@ -1907,7 +2787,10 @@ "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=" + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } }, "ms": { "version": "2.0.0", @@ -1919,19 +2802,42 @@ "version": "0.6.39", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "optional": true + "optional": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } }, "nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "optional": true + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } }, "npmlog": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", - "optional": true + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } }, "number-is-nan": { "version": "1.0.1", @@ -1953,7 +2859,10 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } }, "os-homedir": { "version": "1.0.2", @@ -1971,7 +2880,11 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "optional": true + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } }, "path-is-absolute": { "version": "1.0.1", @@ -2006,6 +2919,12 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, "dependencies": { "minimist": { "version": "1.2.0", @@ -2018,18 +2937,54 @@ "readable-stream": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", - "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=" + "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } }, "request": { "version": "2.81.0", "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "optional": true + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } }, "rimraf": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=" + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "requires": { + "glob": "7.1.2" + } }, "safe-buffer": { "version": "5.0.1", @@ -2057,13 +3012,27 @@ "sntp": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=" + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } }, "sshpk": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -2073,15 +3042,23 @@ } } }, - "string_decoder": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", - "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=" - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=" + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "requires": { + "safe-buffer": "5.0.1" + } }, "stringstream": { "version": "0.0.5", @@ -2092,7 +3069,10 @@ "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } }, "strip-json-comments": { "version": "2.0.1", @@ -2103,25 +3083,46 @@ "tar": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=" + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } }, "tar-pack": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", - "optional": true + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } }, "tough-cookie": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", - "optional": true + "optional": true, + "requires": { + "punycode": "1.4.1" + } }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } }, "tweetnacl": { "version": "0.14.5", @@ -2150,13 +3151,19 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", - "optional": true + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } }, "wide-align": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "optional": true + "optional": true, + "requires": { + "string-width": "1.0.2" + } }, "wrappy": { "version": "1.0.2", @@ -2169,25 +3176,59 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "dev": true + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=" }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true + "dev": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } }, "gaze": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", + "dev": true, + "requires": { + "globule": "1.2.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.2" + } + }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", @@ -2210,35 +3251,74 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } }, "glob-base": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=" + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } }, "glob-parent": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=" + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "2.0.1" + } }, "glob-stream": { "version": "3.1.18", "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-3.1.18.tgz", "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=", "dev": true, + "requires": { + "glob": "4.5.3", + "glob2base": "0.0.12", + "minimatch": "2.0.10", + "ordered-read-streams": "0.1.0", + "through2": "0.6.5", + "unique-stream": "1.0.0" + }, "dependencies": { "glob": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", - "dev": true + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "2.0.10", + "once": "1.4.0" + } }, "isarray": { "version": "0.0.1", @@ -2250,13 +3330,22 @@ "version": "2.0.10", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", - "dev": true + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } }, "readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } }, "string_decoder": { "version": "0.10.31", @@ -2268,7 +3357,11 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } } } }, @@ -2277,24 +3370,40 @@ "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz", "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", "dev": true, + "requires": { + "gaze": "0.5.2" + }, "dependencies": { "gaze": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", - "dev": true + "dev": true, + "requires": { + "globule": "0.1.0" + } }, "glob": { "version": "3.1.21", "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", - "dev": true + "dev": true, + "requires": { + "graceful-fs": "1.2.3", + "inherits": "1.0.2", + "minimatch": "0.2.14" + } }, "globule": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", - "dev": true + "dev": true, + "requires": { + "glob": "3.1.21", + "lodash": "1.0.2", + "minimatch": "0.2.14" + } }, "graceful-fs": { "version": "1.2.3", @@ -2324,7 +3433,11 @@ "version": "0.2.14", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "dev": true + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } } } }, @@ -2332,31 +3445,52 @@ "version": "0.0.12", "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", - "dev": true + "dev": true, + "requires": { + "find-index": "0.1.1" + } }, "global-modules": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", - "dev": true + "dev": true, + "requires": { + "global-prefix": "0.1.5", + "is-windows": "0.2.0" + } }, "global-prefix": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", - "dev": true + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1", + "ini": "1.3.5", + "is-windows": "0.2.0", + "which": "1.3.0" + } }, "globule": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", - "dev": true + "dev": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.5", + "minimatch": "3.0.4" + } }, "glogg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz", "integrity": "sha1-f+DxmfV6yQbPUS/urY+Q7kooT8U=", - "dev": true + "dev": true, + "requires": { + "sparkles": "1.0.0" + } }, "graceful-fs": { "version": "4.1.11", @@ -2368,6 +3502,21 @@ "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.9.1.tgz", "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=", "dev": true, + "requires": { + "archy": "1.0.0", + "chalk": "1.1.3", + "deprecated": "0.0.1", + "gulp-util": "3.0.8", + "interpret": "1.1.0", + "liftoff": "2.3.0", + "minimist": "1.2.0", + "orchestrator": "0.3.8", + "pretty-hrtime": "1.0.3", + "semver": "4.3.6", + "tildify": "1.2.0", + "v8flags": "2.1.1", + "vinyl-fs": "0.3.14" + }, "dependencies": { "ansi-styles": { "version": "2.2.1", @@ -2379,7 +3528,14 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } }, "semver": { "version": "4.3.6", @@ -2399,7 +3555,10 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/gulp-clip-empty-files/-/gulp-clip-empty-files-0.1.2.tgz", "integrity": "sha1-vumATiU7vaVc+8Em6NuuZDPtzMg=", - "dev": true + "dev": true, + "requires": { + "through2": "2.0.3" + } }, "gulp-rename": { "version": "1.2.2", @@ -2412,6 +3571,10 @@ "resolved": "https://registry.npmjs.org/gulp-slash/-/gulp-slash-1.1.3.tgz", "integrity": "sha1-8VUhrCOxeNtE5VHjDi/Lykv2/h0=", "dev": true, + "requires": { + "slash": "0.1.3", + "through2": "0.5.1" + }, "dependencies": { "isarray": { "version": "0.0.1", @@ -2423,7 +3586,13 @@ "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } }, "string_decoder": { "version": "0.10.31", @@ -2435,7 +3604,11 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", - "dev": true + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "3.0.0" + } }, "xtend": { "version": "3.0.0", @@ -2450,6 +3623,26 @@ "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", "dev": true, + "requires": { + "array-differ": "1.0.0", + "array-uniq": "1.0.3", + "beeper": "1.1.1", + "chalk": "1.1.3", + "dateformat": "2.2.0", + "fancy-log": "1.3.1", + "gulplog": "1.0.0", + "has-gulplog": "0.1.0", + "lodash._reescape": "3.0.0", + "lodash._reevaluate": "3.0.0", + "lodash._reinterpolate": "3.0.0", + "lodash.template": "3.6.2", + "minimist": "1.2.0", + "multipipe": "0.1.2", + "object-assign": "3.0.0", + "replace-ext": "0.0.1", + "through2": "2.0.3", + "vinyl": "0.5.3" + }, "dependencies": { "ansi-styles": { "version": "2.2.1", @@ -2461,7 +3654,14 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } }, "object-assign": { "version": "3.0.0", @@ -2481,42 +3681,80 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "dev": true, + "requires": { + "glogg": "1.0.0" + } }, "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "commander": "2.15.1", + "is-my-json-valid": "2.17.2", + "pinkie-promise": "2.0.1" + }, + "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=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } }, "has": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=" + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "requires": { + "function-bind": "1.1.1" + } }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-gulplog": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", - "dev": true + "dev": true, + "requires": { + "sparkles": "1.0.0" + } }, "has-unicode": { "version": "2.0.1", @@ -2528,69 +3766,93 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", - "dev": true + "dev": true, + "requires": { + "inherits": "2.0.3" + } }, "hash.js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", - "dev": true + "integrity": "sha1-NA3tvmKQGHFRweodd3o0SJNd+EY=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } }, "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "dev": true + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true + "dev": true, + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } }, "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==", + "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", "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", - "dev": true + "dev": true, + "requires": { + "parse-passwd": "1.0.0" + } }, "hosted-git-info": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", "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=", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, - "dependencies": { - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", - "dev": true - } + "requires": { + "depd": "1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": "1.4.0" } }, "http-parser-js": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.9.tgz", - "integrity": "sha1-6hoE+2St/wJC6ZdPKX3Uw8rSceE=", + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.11.tgz", + "integrity": "sha512-QCR5O2AjjMW8Mo4HyI1ctFcv+O99j/0g367V3YoVnrNw5hkDvAWZD0lWGcc+F4yN3V55USPCVix4efb75HxFfA==", "dev": true }, "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } }, "https-browserify": { "version": "1.0.0", @@ -2601,13 +3863,13 @@ "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==", + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=", "dev": true }, "ieee754": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", + "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", "dev": true }, "immediate": { @@ -2625,7 +3887,10 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true + "dev": true, + "requires": { + "repeating": "2.0.1" + } }, "indexof": { "version": "0.0.1", @@ -2637,7 +3902,10 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } }, "inherits": { "version": "2.0.3", @@ -2647,8 +3915,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" }, "interpret": { "version": "1.1.0", @@ -2665,7 +3932,7 @@ "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==" + "integrity": "sha1-dQU+SkIqqufXzWaMySz9XLKdOMw=" }, "ionicons": { "version": "3.0.0", @@ -2673,16 +3940,20 @@ "integrity": "sha1-QLja9P16MRUL0AIWD2ZJbiKpjDw=" }, "ipaddr.js": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=", "dev": true }, "is-absolute": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", "integrity": "sha1-IN5p89uULvLYe5wto28XIjWxtes=", - "dev": true + "dev": true, + "requires": { + "is-relative": "0.2.1", + "is-windows": "0.2.0" + } }, "is-arrayish": { "version": "0.2.1", @@ -2693,24 +3964,38 @@ "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=" + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "1.11.0" + } }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" }, "is-builtin-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } }, "is-callable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "requires": { + "ci-info": "1.1.3" + } + }, "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", @@ -2721,10 +4006,18 @@ "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" }, + "is-electron-renderer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz", + "integrity": "sha1-pGnQVvl1aXxYyYxgI+sKp5r4laI=" + }, "is-equal-shallow": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=" + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "2.0.0" + } }, "is-extendable": { "version": "0.1.1", @@ -2740,18 +4033,27 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } }, "is-glob": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=" + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "1.0.0" + } }, "is-module": { "version": "1.0.0", @@ -2759,16 +4061,41 @@ "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.2.0", + "is-my-ip-valid": "1.0.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=" + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "3.2.2" + } }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "dev": true, + "requires": { + "isobject": "3.0.1" + }, "dependencies": { "isobject": { "version": "3.0.1", @@ -2788,16 +4115,28 @@ "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", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=" + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "1.0.1" + } }, "is-relative": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", "integrity": "sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU=", - "dev": true + "dev": true, + "requires": { + "is-unc-path": "0.1.2" + } }, "is-stream": { "version": "1.1.0", @@ -2820,7 +4159,10 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", "integrity": "sha1-arBTpyVzwQJQ/0FqOBTDUXivObk=", - "dev": true + "dev": true, + "requires": { + "unc-path-regex": "0.1.2" + } }, "is-utf8": { "version": "0.2.1", @@ -2848,7 +4190,10 @@ "isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=" + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } }, "isstream": { "version": "0.1.2", @@ -2857,9 +4202,9 @@ "dev": true }, "js-base64": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz", - "integrity": "sha512-Wehd+7Pf9tFvGb+ydPm9TjYjV8X1YHOVyG8QyELZxEMqOhemVwGRmoG8iQ/soqI3n8v4xn59zaLxiCJiaaRzKA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", + "integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==", "dev": true }, "js-tokens": { @@ -2868,6 +4213,15 @@ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", "dev": true }, + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -2878,7 +4232,7 @@ "json-loader": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", + "integrity": "sha1-3KFKcCNf+C8KyaOr62DTN6NlGF0=", "dev": true }, "json-schema": { @@ -2909,23 +4263,60 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "4.1.11" + } + }, + "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", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } }, "jszip": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz", - "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==", + "integrity": "sha1-48KmxtcGrG5gMxQDbUPNQL7v3zc=", + "requires": { + "core-js": "2.3.0", + "es6-promise": "3.0.2", + "lie": "3.1.1", + "pako": "1.0.6", + "readable-stream": "2.0.6" + }, "dependencies": { "readable-stream": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=" + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } }, "string_decoder": { "version": "0.10.31", @@ -2937,7 +4328,10 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } }, "lazy-cache": { "version": "1.0.4", @@ -2945,34 +4339,71 @@ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", "dev": true }, + "lazy-val": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz", + "integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg==" + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "2.3.3" + } + }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } }, "lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=" + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "requires": { + "immediate": "3.0.6" + } }, "liftoff": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.3.0.tgz", "integrity": "sha1-qY8v9nGD2Lp8+soQVIvX/wVQs4U=", - "dev": true + "dev": true, + "requires": { + "extend": "3.0.1", + "findup-sync": "0.4.3", + "fined": "1.1.0", + "flagged-respawn": "0.3.2", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", + "lodash.mapvalues": "4.6.0", + "rechoir": "0.6.2", + "resolve": "1.5.0" + } }, "livereload-js": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz", - "integrity": "sha1-bIclfmSKtHW8JOoldFftzB+NC8I=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.3.0.tgz", + "integrity": "sha512-j1R0/FeGa64Y+NmqfZhyoVRzcFlOZ8sNlKzHjh4VvLULFACZhn68XrX5DFg2FhMvSMJmROuFxRSa560ECWKBMg==", "dev": true }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } }, "loader-runner": { "version": "2.3.0", @@ -2984,13 +4415,22 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, "dependencies": { "path-exists": { "version": "3.0.0", @@ -3001,10 +4441,9 @@ } }, "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" }, "lodash._basecopy": { "version": "3.0.1", @@ -3076,7 +4515,10 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", - "dev": true + "dev": true, + "requires": { + "lodash._root": "3.0.1" + } }, "lodash.isarguments": { "version": "3.1.0", @@ -3106,7 +4548,12 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } }, "lodash.mapvalues": { "version": "4.6.0", @@ -3115,9 +4562,9 @@ "dev": true }, "lodash.mergewith": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz", - "integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", + "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", "dev": true }, "lodash.restparam": { @@ -3130,13 +4577,28 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", - "dev": true + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash._basetostring": "3.0.1", + "lodash._basevalues": "3.0.0", + "lodash._isiterateecall": "3.0.9", + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0", + "lodash.keys": "3.1.2", + "lodash.restparam": "3.6.1", + "lodash.templatesettings": "3.1.1" + } }, "lodash.templatesettings": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", - "dev": true + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0" + } }, "longest": { "version": "1.0.1", @@ -3148,25 +4610,36 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } }, "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "dev": true + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", + "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } }, "macos-release": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-1.1.0.tgz", - "integrity": "sha512-mmLbumEYMi5nXReB9js3WGsB8UE6cDBWyIO62Z4DNx6GbRhDxHNjA1MlzSpJ2S2KM1wyiPRA0d19uHWYYvMHjA==", + "integrity": "sha1-gxlF4pNltHCqhySwqzbI+JWdEPs=", "dev": true }, "magic-string": { - "version": "0.22.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.4.tgz", - "integrity": "sha512-kxBL06p6iO2qPBHsqGK2b3cRwiRGpnmSuVWNhwHcMX7qJOUr1HvricYP1LZOCdkQBUp0jiWg2d6WJwR3vYgByw==", - "dev": true + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", + "dev": true, + "requires": { + "vlq": "0.2.3" + } }, "map-cache": { "version": "0.2.2", @@ -3185,12 +4658,20 @@ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + }, "dependencies": { "hash-base": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "dev": true + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } } } }, @@ -3204,19 +4685,38 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true + "dev": true, + "requires": { + "errno": "0.1.7", + "readable-stream": "2.3.3" + } }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } }, "merge-descriptors": { "version": "1.0.1", @@ -3233,36 +4733,58 @@ "micromatch": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=" + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } }, "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true + "integrity": "sha1-8IA1HIZbDcViqEYpZtqlNUPHik0=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } }, "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=", "dev": true }, "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "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==", "dev": true }, "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "dev": true + "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==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } }, "mimic-fn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", - "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, "minimalistic-assert": { @@ -3280,7 +4802,10 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==" + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "1.1.8" + } }, "minimist": { "version": "1.2.0", @@ -3291,6 +4816,9 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, "dependencies": { "minimist": { "version": "0.0.8", @@ -3307,14 +4835,16 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multipipe": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", - "dev": true + "dev": true, + "requires": { + "duplexer2": "0.0.2" + } }, "nan": { "version": "2.8.0", @@ -3333,11 +4863,38 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true }, + "neo-async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.0.tgz", + "integrity": "sha512-nJmSswG4As/MkRq7QZFuH/sf/yuv8ODdMZrY4Bedjp77a5MK4A6s7YbBB64c9u79EBUOfXUXBvArmvzTD0X+6g==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, "node-gyp": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", "dev": true, + "requires": { + "fstream": "1.0.11", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.1.2", + "osenv": "0.1.5", + "request": "2.79.0", + "rimraf": "2.6.2", + "semver": "5.3.0", + "tar": "2.2.1", + "which": "1.3.0" + }, "dependencies": { "semver": { "version": "5.3.0", @@ -3350,14 +4907,71 @@ "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==", - "dev": true + "integrity": "sha1-X5QmPUBPbkR2fXJpAf/wVHjWAN8=", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.2.0", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "domain-browser": "1.2.0", + "events": "1.1.1", + "https-browserify": "1.0.0", + "os-browserify": "0.3.0", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.3", + "stream-browserify": "2.0.1", + "stream-http": "2.8.1", + "string_decoder": "1.1.1", + "timers-browserify": "2.0.6", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } }, "node-sass": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.5.3.tgz", - "integrity": "sha1-0JydEXlkEjnRuX/8YjH9zsU+FWg=", + "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==", "dev": true, + "requires": { + "async-foreach": "0.1.3", + "chalk": "1.1.3", + "cross-spawn": "3.0.1", + "gaze": "1.1.2", + "get-stdin": "4.0.1", + "glob": "7.1.2", + "in-publish": "2.0.0", + "lodash.assign": "4.2.0", + "lodash.clonedeep": "4.5.0", + "lodash.mergewith": "4.6.1", + "meow": "3.7.0", + "mkdirp": "0.5.1", + "nan": "2.8.0", + "node-gyp": "3.6.2", + "npmlog": "4.1.2", + "request": "2.79.0", + "sass-graph": "2.2.4", + "stdout-stream": "1.4.0", + "true-case-path": "1.0.2" + }, "dependencies": { "ansi-styles": { "version": "2.2.1", @@ -3369,13 +4983,24 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true + "dev": true, + "requires": { + "lru-cache": "4.1.2", + "which": "1.3.0" + } }, "supports-color": { "version": "2.0.0", @@ -3389,18 +5014,30 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true + "dev": true, + "requires": { + "abbrev": "1.1.1" + } }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "dev": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } }, "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=" + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "1.1.0" + } }, "normalize-range": { "version": "0.1.2", @@ -3412,13 +5049,22 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true + "dev": true, + "requires": { + "path-key": "2.0.1" + } }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true + "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "dev": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } }, "num2fraction": { "version": "1.2.2", @@ -3454,12 +5100,21 @@ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", "dev": true, + "requires": { + "array-each": "1.0.1", + "array-slice": "1.1.0", + "for-own": "1.0.0", + "isobject": "3.0.1" + }, "dependencies": { "for-own": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true + "dev": true, + "requires": { + "for-in": "1.0.2" + } }, "isobject": { "version": "3.0.1", @@ -3472,13 +5127,20 @@ "object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=" + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "dev": true, + "requires": { + "isobject": "3.0.1" + }, "dependencies": { "isobject": { "version": "3.0.1", @@ -3492,31 +5154,47 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true + "dev": true, + "requires": { + "ee-first": "1.1.1" + } }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true + "requires": { + "wrappy": "1.0.2" + } }, "orchestrator": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.8.tgz", "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=", "dev": true, + "requires": { + "end-of-stream": "0.1.5", + "sequencify": "0.0.7", + "stream-consume": "0.1.0" + }, "dependencies": { "end-of-stream": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=", - "dev": true + "dev": true, + "requires": { + "once": "1.3.3" + } }, "once": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", - "dev": true + "dev": true, + "requires": { + "wrappy": "1.0.2" + } } } }, @@ -3542,13 +5220,20 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true + "dev": true, + "requires": { + "lcid": "1.0.0" + } }, "os-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/os-name/-/os-name-2.0.1.tgz", "integrity": "sha1-uaOGNhwXrjohc27wWZQFyajF3F4=", - "dev": true + "dev": true, + "requires": { + "macos-release": "1.1.0", + "win-release": "1.1.1" + } }, "os-tmpdir": { "version": "1.0.2", @@ -3557,10 +5242,14 @@ "dev": true }, "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "dev": true + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } }, "p-finally": { "version": "1.0.0", @@ -3569,44 +5258,77 @@ "dev": true }, "p-limit": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", - "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "dev": true, + "requires": { + "p-try": "1.0.0" + } }, "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, "pako": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" + "integrity": "sha1-AQEhG6pwxLykoPY/Igbpe3368lg=" }, "parse-asn1": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", - "dev": true + "dev": true, + "requires": { + "asn1.js": "4.10.1", + "browserify-aes": "1.2.0", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.14" + } }, "parse-filepath": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.1.tgz", "integrity": "sha1-FZ1hVdQ5BNFsEO9piRHaHpGWm3M=", - "dev": true + "dev": true, + "requires": { + "is-absolute": "0.2.6", + "map-cache": "0.2.2", + "path-root": "0.1.1" + } }, "parse-glob": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=" + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true + "dev": true, + "requires": { + "error-ex": "1.3.1" + } }, "parse-passwd": { "version": "1.0.0", @@ -3630,7 +5352,10 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } }, "path-is-absolute": { "version": "1.0.1", @@ -3653,7 +5378,10 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "dev": true + "dev": true, + "requires": { + "path-root-regex": "0.1.2" + } }, "path-root-regex": { "version": "0.1.2", @@ -3665,6 +5393,9 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, "dependencies": { "isarray": { "version": "0.0.1", @@ -3677,19 +5408,25 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } }, "pbkdf2": { "version": "3.0.14", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", - "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-o14TxkeZsGzhUyD0WcIw5o5zut4=", + "dev": true, + "requires": { + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.11" + } }, "pify": { "version": "2.3.0", @@ -3707,20 +5444,20 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true + "dev": true, + "requires": { + "pinkie": "2.0.4" + } }, "postcss": { - "version": "6.0.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", - "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.21.tgz", + "integrity": "sha512-y/bKfbQz2Nn/QBC08bwvYUxEFOVGfPIUOTsJ2CK5inzlXW9SdYR1x4pEsG9blRAF/PX+wRNdOah+gx/hv4q7dw==", "dev": true, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "requires": { + "chalk": "2.3.2", + "source-map": "0.5.7", + "supports-color": "5.3.0" } }, "postcss-value-parser": { @@ -3754,13 +5491,22 @@ "promise.prototype.finally": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.0.tgz", - "integrity": "sha512-7p/K2f6dI+dM8yjRQEGrTQs5hTQixUAdOGpMEA3+pVxpX5oHKRSKAXyLw9Q9HUWDTdwtoo39dSHGQtN90HcEwQ==" + "integrity": "sha1-ZvFhsWQ2NuUOfPIB3BuEqFfzhk4=", + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0", + "function-bind": "1.1.1" + } }, "proxy-addr": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "dev": true, + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.6.0" + } }, "proxy-middleware": { "version": "0.15.0", @@ -3784,7 +5530,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", - "dev": true + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "parse-asn1": "5.1.0", + "randombytes": "2.0.6" + } }, "punycode": { "version": "1.4.1", @@ -3795,7 +5548,7 @@ "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=", "dev": true }, "querystring": { @@ -3813,38 +5566,58 @@ "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, "dependencies": { "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=" + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.6" + } } } }, "randombytes": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", - "dev": true + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } }, "randomfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz", - "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==", - "dev": true + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "2.0.6", + "safe-buffer": "5.1.1" + } }, "range-parser": { "version": "1.2.0", @@ -3856,41 +5629,114 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "dev": true + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "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.4.0" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + } + } }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } }, "read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } }, "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==" + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + }, + "dependencies": { + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + } + } }, "readdirp": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=" + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true + "dev": true, + "requires": { + "resolve": "1.5.0" + } }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } }, "reflect-metadata": { "version": "0.1.10", @@ -3900,7 +5746,10 @@ "regex-cache": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==" + "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=", + "requires": { + "is-equal-shallow": "0.1.3" + } }, "remove-trailing-separator": { "version": "1.1.0", @@ -3921,7 +5770,10 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true + "dev": true, + "requires": { + "is-finite": "1.0.2" + } }, "replace-ext": { "version": "0.0.1", @@ -3930,10 +5782,40 @@ "dev": true }, "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "dev": true + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.7.0", + "caseless": "0.11.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "qs": "6.3.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.4.3", + "uuid": "3.2.1" + }, + "dependencies": { + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true + } + } }, "require-directory": { "version": "2.1.1", @@ -3950,56 +5832,90 @@ "resolve": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", - "dev": true + "integrity": "sha1-HwmsznlsmnYlefMbLBzEw83fnzY=", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } }, "resolve-dir": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", - "dev": true + "dev": true, + "requires": { + "expand-tilde": "1.2.2", + "global-modules": "0.2.3" + } }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true + "dev": true, + "requires": { + "align-text": "0.1.4" + } }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "dev": true, + "requires": { + "glob": "7.1.2" + } }, "ripemd160": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", - "dev": true + "dev": true, + "requires": { + "hash-base": "2.0.2", + "inherits": "2.0.3" + } }, "rollup": { "version": "0.50.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.50.0.tgz", - "integrity": "sha512-7RqCBQ9iwsOBPkjYgoIaeUij606mSkDMExP0NT7QDI3bqkHYQHrQ83uoNIXwPcQm/vP2VbsUz3kiyZZ1qPlLTQ==", + "integrity": "sha1-TBWPTngObLM/8Nv8GEpSzFjNXzs=", "dev": true }, "rollup-plugin-commonjs": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.2.6.tgz", - "integrity": "sha512-qK0+uhktmnAgZkHkqFuajNmPw93fjrO7+CysDaxWE5jrUR9XSlSvuao5ZJP+XizxA8weakhgYYBtbVz9SGBpjA==", - "dev": true + "integrity": "sha1-J+W5Bp/5QAW7AeAbtGoeSHN4Rnc=", + "dev": true, + "requires": { + "acorn": "5.5.3", + "estree-walker": "0.5.1", + "magic-string": "0.22.5", + "resolve": "1.5.0", + "rollup-pluginutils": "2.0.1" + } }, "rollup-plugin-node-resolve": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.0.0.tgz", "integrity": "sha1-i4l8TDAw1QASd7BRSyXSygloPuA=", - "dev": true + "dev": true, + "requires": { + "browser-resolve": "1.11.2", + "builtin-modules": "1.1.1", + "is-module": "1.0.0", + "resolve": "1.5.0" + } }, "rollup-pluginutils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz", "integrity": "sha1-fslbNXP2VDpGpkYb2afFRFJdD8A=", "dev": true, + "requires": { + "estree-walker": "0.3.1", + "micromatch": "2.3.11" + }, "dependencies": { "estree-walker": { "version": "0.3.1", @@ -4012,12 +5928,15 @@ "rxjs": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.2.tgz", - "integrity": "sha512-oRYoIKWBU3Ic37fLA5VJu31VqQO4bWubRntcHSJ+cwaDQBwdnZ9x4zmhJfm/nFQ2E82/I4loSioHnACamrKGgA==" + "integrity": "sha1-KNQD8AcRIZZ/GK1mVWMlXVQjasM=", + "requires": { + "symbol-observable": "1.1.0" + } }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" }, "safe-json-parse": { "version": "1.0.1", @@ -4025,50 +5944,70 @@ "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=", "dev": true }, + "sanitize-filename": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", + "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=", + "requires": { + "truncate-utf8-bytes": "1.0.2" + } + }, + "sanitize-xml-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sanitize-xml-string/-/sanitize-xml-string-1.1.0.tgz", + "integrity": "sha1-3YZoYGnOECtJVJEtnHmCA3uSh2g=" + }, "sass-graph": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", - "dev": true + "dev": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.5", + "scss-tokenizer": "0.2.3", + "yargs": "7.1.0" + } }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=" }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", "dev": true, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true - } + "requires": { + "js-base64": "2.4.3", + "source-map": "0.5.7" } }, "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "send": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", - "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", "dev": true, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true - } + "requires": { + "debug": "2.6.9", + "depd": "1.1.2", + "destroy": "1.0.4", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.3", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.4.0" } }, "sequencify": { @@ -4078,10 +6017,16 @@ "dev": true }, "serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, + "requires": { + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.2" + } }, "serviceworker-cache-polyfill": { "version": "4.0.0", @@ -4108,20 +6053,27 @@ "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=", "dev": true }, "sha.js": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz", - "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==", - "dev": true + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } }, "shebang-regex": { "version": "1.0.0", @@ -4148,15 +6100,18 @@ "dev": true }, "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "dev": true + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } }, "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==", + "integrity": "sha1-qqR0A/eyRakvvJfqCPJQ1gh+0IU=", "dev": true }, "source-map": { @@ -4167,7 +6122,10 @@ "source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==" + "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=", + "requires": { + "source-map": "0.5.7" + } }, "sparkles": { "version": "1.0.0", @@ -4176,46 +6134,95 @@ "dev": true }, "spdx-correct": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", "dev": true }, "spdx-expression-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } }, "spdx-license-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", "dev": true }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "dev": true + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "stat-mode": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", + "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=" }, "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", "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=", - "dev": true + "dev": true, + "requires": { + "readable-stream": "2.3.3" + } }, "stream-browserify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", - "dev": true + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } }, "stream-consume": { "version": "0.1.0", @@ -4224,15 +6231,17 @@ "dev": true }, "stream-http": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", - "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", - "dev": true - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.1.tgz", + "integrity": "sha512-cQ0jo17BLca2r0GfRdZKYAGLU6JRoIWxqSOakUMuKOT6MOK7AAlE856L33QuDmAy/eeOrhLee3dZKX0Uadu93A==", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } }, "string-template": { "version": "0.2.1", @@ -4244,6 +6253,17 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, "stringstream": { @@ -4256,13 +6276,19 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } }, "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } }, "strip-eof": { "version": "1.0.0", @@ -4274,23 +6300,32 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } }, "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "requires": { + "has-flag": "3.0.0" + } }, "sw-toolbox": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/sw-toolbox/-/sw-toolbox-3.6.0.tgz", - "integrity": "sha1-Jt8dHHA0hljk3qKIQxkUm3sxg7U=" + "integrity": "sha1-Jt8dHHA0hljk3qKIQxkUm3sxg7U=", + "requires": { + "path-to-regexp": "1.7.0", + "serviceworker-cache-polyfill": "4.0.0" + } }, "symbol-observable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.1.0.tgz", - "integrity": "sha512-dQoid9tqQ+uotGhuTKEY11X4xhyYePVnqGSoSm3OGKh2E8LZ6RPULp1uXTctk33IeERlrRJYoVSBglsL05F5Uw==" + "integrity": "sha1-XGj9jVQRXZ37cqhHIFSSIujbmzI=" }, "tapable": { "version": "0.2.8", @@ -4302,7 +6337,34 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "dev": true + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-stream": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", + "integrity": "sha1-XK2Ed59FyDsfJQjZawnYjHIYr1U=", + "requires": { + "bl": "1.2.2", + "end-of-stream": "1.4.1", + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } + }, + "temp-file": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.1.1.tgz", + "integrity": "sha512-W/6SJgtg2SE/5rxgwUwoDhdSXrvUWQBpgKJglaAe6S7mk1kLkI+LUbY/jPZBu3UhydDJZstNNd7AJhnZ0UZHtw==", + "requires": { + "async-exit-hook": "2.0.1", + "bluebird-lst": "1.0.5", + "fs-extra-p": "4.5.2", + "lazy-val": "1.0.3" + } }, "through": { "version": "2.3.8", @@ -4314,13 +6376,20 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "dev": true + "dev": true, + "requires": { + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } }, "tildify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", - "dev": true + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } }, "time-stamp": { "version": "1.1.0", @@ -4329,22 +6398,36 @@ "dev": true }, "timers-browserify": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", - "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", - "dev": true + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.6.tgz", + "integrity": "sha512-HQ3nbYRAowdVd0ckGFvmJPPCOH/CHleFN/Y0YQCX1DVaB7t+KFvisuyN09fuP8Jtp1CpfSh8O8bMkHbdbPe6Pw==", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } }, "tiny-lr": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.0.5.tgz", - "integrity": "sha512-YrxUSiMgOVh3PnAqtdAUQuUVEVRnqcRCxJ3BHrl/aaWV2fplKKB60oClM0GH2Gio2hcXvkxMUxsC/vXZrQePlg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", + "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", "dev": true, + "requires": { + "body": "5.1.0", + "debug": "3.1.0", + "faye-websocket": "0.10.0", + "livereload-js": "2.3.0", + "object-assign": "4.1.1", + "qs": "6.5.1" + }, "dependencies": { "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "dev": true, + "requires": { + "ms": "2.0.0" + } } } }, @@ -4355,10 +6438,13 @@ "dev": true }, "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "dev": true + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "1.4.1" + } }, "trim-newlines": { "version": "1.0.0", @@ -4366,6 +6452,38 @@ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "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=", + "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.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "requires": { + "utf8-byte-length": "1.0.4" + } + }, "ts-md5": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.2.tgz", @@ -4374,7 +6492,13 @@ "tsickle": { "version": "0.24.1", "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.24.1.tgz", - "integrity": "sha512-XloFQZhVhgjpQsi3u2ORNRJvuID5sflOg6HfP093IqAbhE1+fIUXznULpdDwHgG4p+v8w78KdHruQtkWUKx5AQ==" + "integrity": "sha1-A5NDsgW/UXozOwcDl4iS+Ap9hI4=", + "requires": { + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map": "0.5.7", + "source-map-support": "0.4.18" + } }, "tslib": { "version": "1.8.1", @@ -4382,16 +6506,35 @@ "integrity": "sha1-aUavLR1lGnsYY7Ux1uWvpBqkTqw=" }, "tslint": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.8.0.tgz", - "integrity": "sha1-H0mtWy53x2w69N3K5VKuTjYS6xM=", - "dev": true + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.3.2", + "commander": "2.15.1", + "diff": "3.5.0", + "glob": "7.1.2", + "js-yaml": "3.11.0", + "minimatch": "3.0.4", + "resolve": "1.5.0", + "semver": "5.5.0", + "tslib": "1.8.1", + "tsutils": "2.26.1" + } }, "tslint-eslint-rules": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-4.1.1.tgz", "integrity": "sha1-fDDniC8mvCdr/5HSOEl1xp2viLo=", "dev": true, + "requires": { + "doctrine": "0.7.2", + "tslib": "1.8.1", + "tsutils": "1.9.1" + }, "dependencies": { "tsutils": { "version": "1.9.1", @@ -4402,10 +6545,13 @@ } }, "tsutils": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.13.0.tgz", - "integrity": "sha512-FuWzNJbMsp3gcZMbI3b5DomhW4Ia41vMxjN63nKWI0t7f+I3UmHfRl0TrXJTwI2LUduDG+eR1Mksp3pvtlyCFQ==", - "dev": true + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.26.1.tgz", + "integrity": "sha512-bnm9bcjOqOr1UljleL94wVCDlpa6KjfGaTkefeLch4GRafgDkROxPizbB/FxTEdI++5JqhxczRy/Qub0syNqZA==", + "dev": true, + "requires": { + "tslib": "1.8.1" + } }, "tty-browserify": { "version": "0.0.0", @@ -4414,9 +6560,9 @@ "dev": true }, "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", "dev": true }, "tweetnacl": { @@ -4427,10 +6573,14 @@ "optional": true }, "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "dev": true + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.18" + } }, "typescript": { "version": "2.4.2", @@ -4441,13 +6591,17 @@ "uglify-es": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.2.2.tgz", - "integrity": "sha512-l+s5VLzFwGJfS+fbqaGf/Dfwo1MF13jLOF2ekL0PytzqEqQ6cVppvHf4jquqFok+35USMpKjqkYxy6pQyUcuug==", + "integrity": "sha1-FcYrd3UALIG3mHocSezT8SbKznM=", "dev": true, + "requires": { + "commander": "2.12.2", + "source-map": "0.5.7" + }, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", "dev": true } } @@ -4464,6 +6618,11 @@ "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", "dev": true, + "requires": { + "source-map": "0.5.7", + "uglify-js": "2.8.29", + "webpack-sources": "1.1.0" + }, "dependencies": { "camelcase": { "version": "1.2.1", @@ -4475,26 +6634,42 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true + "dev": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + } }, "yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } } } }, "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=", "dev": true }, "unc-path-regex": { @@ -4512,8 +6687,7 @@ "universalify": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", - "dev": true + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" }, "unpipe": { "version": "1.0.0", @@ -4526,6 +6700,10 @@ "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, "dependencies": { "punycode": { "version": "1.3.2", @@ -4541,18 +6719,18 @@ "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", "dev": true }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" + }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - } + "requires": { + "inherits": "2.0.3" } }, "util-deprecate": { @@ -4567,22 +6745,28 @@ "dev": true }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", - "dev": true + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, "v8flags": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", - "dev": true + "dev": true, + "requires": { + "user-home": "1.1.1" + } }, "validate-npm-package-license": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", - "dev": true + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } }, "vary": { "version": "1.1.2", @@ -4594,19 +6778,47 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } }, "vinyl": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", - "dev": true + "dev": true, + "requires": { + "clone": "1.0.3", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } }, "vinyl-fs": { "version": "0.3.14", "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-0.3.14.tgz", "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=", "dev": true, + "requires": { + "defaults": "1.0.3", + "glob-stream": "3.1.18", + "glob-watcher": "0.0.6", + "graceful-fs": "3.0.11", + "mkdirp": "0.5.1", + "strip-bom": "1.0.0", + "through2": "0.6.5", + "vinyl": "0.4.6" + }, "dependencies": { "clone": { "version": "0.2.0", @@ -4618,7 +6830,10 @@ "version": "3.0.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "dev": true + "dev": true, + "requires": { + "natives": "1.1.1" + } }, "isarray": { "version": "0.0.1", @@ -4630,7 +6845,13 @@ "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } }, "string_decoder": { "version": "0.10.31", @@ -4642,71 +6863,276 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz", "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", - "dev": true + "dev": true, + "requires": { + "first-chunk-stream": "1.0.0", + "is-utf8": "0.2.1" + } }, "through2": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } }, "vinyl": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "dev": true + "dev": true, + "requires": { + "clone": "0.2.0", + "clone-stats": "0.0.1" + } } } }, "vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", - "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==", + "integrity": "sha1-jz5DKM9jsVQMDWfhsneDhviXWyY=", "dev": true }, "vm-browserify": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true + "dev": true, + "requires": { + "indexof": "0.0.1" + } }, "watchpack": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", - "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.5.0.tgz", + "integrity": "sha512-RSlipNQB1u48cq0wH/BNfCu1tD/cJ8ydFIkNYhp9o+3d+8unClkIovpW5qpFPgmL9OE48wfAnlZydXByWP82AA==", + "dev": true, + "requires": { + "chokidar": "1.7.0", + "graceful-fs": "4.1.11", + "neo-async": "2.5.0" + } + }, + "webpack": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz", + "integrity": "sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==", + "dev": true, + "requires": { + "acorn": "5.5.3", + "acorn-dynamic-import": "2.0.2", + "ajv": "5.5.2", + "ajv-keywords": "2.1.1", + "async": "2.6.0", + "enhanced-resolve": "3.4.1", + "escope": "3.6.0", + "interpret": "1.1.0", + "json-loader": "0.5.7", + "json5": "0.5.1", + "loader-runner": "2.3.0", + "loader-utils": "1.1.0", + "memory-fs": "0.4.1", + "mkdirp": "0.5.1", + "node-libs-browser": "2.1.0", + "source-map": "0.5.7", + "supports-color": "4.5.0", + "tapable": "0.2.8", + "uglifyjs-webpack-plugin": "0.4.6", + "watchpack": "1.5.0", + "webpack-sources": "1.1.0", + "yargs": "8.0.2" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "dev": true, + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "webpack-merge": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.2.tgz", + "integrity": "sha1-XTct3dPh5fiHT1v1qOkp2wn+shY=", + "dev": true, + "requires": { + "lodash": "4.17.5" + } }, "webpack-sources": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", - "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "integrity": "sha1-oQHrrlnWUHNU1x2AE5UKOot6WlQ=", "dev": true, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "requires": { + "source-list-map": "2.0.0", + "source-map": "0.5.7" } }, "websocket-driver": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", - "dev": true + "dev": true, + "requires": { + "http-parser-js": "0.4.11", + "websocket-extensions": "0.1.3" + } }, "websocket-extensions": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "integrity": "sha1-XS/yKXcAPsaHpLhwc9+7rBRszyk=", "dev": true }, "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "dev": true + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } }, "which-module": { "version": "1.0.0", @@ -4717,14 +7143,20 @@ "wide-align": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "dev": true + "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=", + "dev": true, + "requires": { + "string-width": "1.0.2" + } }, "win-release": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/win-release/-/win-release-1.1.1.tgz", "integrity": "sha1-X6VeAr58qTTt/BJmVjLoSbcuUgk=", - "dev": true + "dev": true, + "requires": { + "semver": "5.5.0" + } }, "window-size": { "version": "0.1.0", @@ -4742,37 +7174,53 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz", - "integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==", - "dev": true + "integrity": "sha1-lsHQiz/v2h1cHjNwDTv6qb4tVgg=", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + }, + "xml-escape": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.1.0.tgz", + "integrity": "sha1-OQTBQ/qOs6ADDsZG0pAqLxtwbEQ=" }, "xml2js": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "dev": true + "integrity": "sha1-aGwg8hMgnpSr8NG88e+qKRx4J6c=", + "dev": true, + "requires": { + "sax": "1.2.4", + "xmlbuilder": "9.0.7" + } }, "xmlbuilder": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz", - "integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8=", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", "dev": true }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "y18n": { "version": "3.2.1", @@ -4791,6 +7239,21 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + }, "dependencies": { "camelcase": { "version": "3.0.0", @@ -4805,6 +7268,9 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", "dev": true, + "requires": { + "camelcase": "3.0.0" + }, "dependencies": { "camelcase": { "version": "3.0.0", @@ -4814,10 +7280,21 @@ } } }, + "zip-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", + "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", + "requires": { + "archiver-utils": "1.3.0", + "compress-commons": "1.2.2", + "lodash": "4.17.5", + "readable-stream": "2.3.3" + } + }, "zone.js": { "version": "0.8.18", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.18.tgz", - "integrity": "sha512-knKOBQM0oea3/x9pdyDuDi7RhxDlJhOIkeixXSiTKWLgs4LpK37iBc+1HaHwzlciHUKT172CymJFKo8Xgh+44Q==" + "integrity": "sha1-jOyzl3/NGzCQVi/0Vw4oR+dStI0=" } } } diff --git a/package.json b/package.json index 0342fe4bb..cc0359250 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "zone.js": "0.8.18" }, "devDependencies": { - "@ionic/app-scripts": "^3.1.5", + "@ionic/app-scripts": "^3.1.8", "gulp": "^3.9.1", "gulp-clip-empty-files": "^0.1.2", "gulp-rename": "^1.2.2", diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index d29d7b0ea..ebea96e97 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -518,18 +518,18 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.domUtils.showConfirm(this.translate.instant(langKey)).then(() => { const modal = this.domUtils.showModalLoading('core.deleting', true); - this.messagesProvider.deleteMessage(message).then(() => { + return this.messagesProvider.deleteMessage(message).then(() => { // Remove message from the list without having to wait for re-fetch. this.messages.splice(index, 1); this.removeMessage(message.hash); this.notifyNewMessage(); this.fetchData(); // Re-fetch messages to update cached data. - }).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'addon.messages.errordeletemessage', true); }).finally(() => { modal.dismiss(); }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errordeletemessage', true); }); } diff --git a/src/addon/messages/providers/sync.ts b/src/addon/messages/providers/sync.ts index 6e2dfcbfe..0cc5a6c5b 100644 --- a/src/addon/messages/providers/sync.ts +++ b/src/addon/messages/providers/sync.ts @@ -34,12 +34,12 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { static AUTO_SYNCED = 'addon_messages_autom_synced'; - constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, - protected appProvider: CoreAppProvider, private messagesOffline: AddonMessagesOfflineProvider, - private eventsProvider: CoreEventsProvider, private messagesProvider: AddonMessagesProvider, - private userProvider: CoreUserProvider, private translate: TranslateService, private utils: CoreUtilsProvider, - syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider) { - super('AddonMessagesSync', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils); + constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, + translate: TranslateService, syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, + private messagesOffline: AddonMessagesOfflineProvider, private eventsProvider: CoreEventsProvider, + private messagesProvider: AddonMessagesProvider, private userProvider: CoreUserProvider, + private utils: CoreUtilsProvider) { + super('AddonMessagesSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); } /** diff --git a/src/addon/messages/providers/user-add-contact-handler.ts b/src/addon/messages/providers/user-add-contact-handler.ts index 8c1e8241e..0dac7a96d 100644 --- a/src/addon/messages/providers/user-add-contact-handler.ts +++ b/src/addon/messages/providers/user-add-contact-handler.ts @@ -100,8 +100,6 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle return this.domUtils.showConfirm(template, title, title).then(() => { return this.messagesProvider.removeContact(user.id); - }, () => { - // Ignore on cancel. }); } else { return this.messagesProvider.addContact(user.id); diff --git a/src/addon/messages/providers/user-block-contact-handler.ts b/src/addon/messages/providers/user-block-contact-handler.ts index 1693dd538..a329c06d2 100644 --- a/src/addon/messages/providers/user-block-contact-handler.ts +++ b/src/addon/messages/providers/user-block-contact-handler.ts @@ -103,8 +103,6 @@ export class AddonMessagesBlockContactUserHandler implements CoreUserProfileHand return this.domUtils.showConfirm(template, title, title).then(() => { return this.messagesProvider.blockContact(user.id); - }, () => { - // Ignore on cancel. }); } }).catch((error) => { diff --git a/src/addon/mod/quiz/accessrules/delaybetweenattempts/delaybetweenattempts.module.ts b/src/addon/mod/quiz/accessrules/delaybetweenattempts/delaybetweenattempts.module.ts new file mode 100644 index 000000000..6d0317da2 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/delaybetweenattempts/delaybetweenattempts.module.ts @@ -0,0 +1,30 @@ +// (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 { AddonModQuizAccessDelayBetweenAttemptsHandler } from './providers/handler'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + ], + providers: [ + AddonModQuizAccessDelayBetweenAttemptsHandler + ] +}) +export class AddonModQuizAccessDelayBetweenAttemptsModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessDelayBetweenAttemptsHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/accessrules/delaybetweenattempts/providers/handler.ts b/src/addon/mod/quiz/accessrules/delaybetweenattempts/providers/handler.ts new file mode 100644 index 000000000..d102e9c52 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/delaybetweenattempts/providers/handler.ts @@ -0,0 +1,52 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; + +/** + * Handler to support delay between attempts access rule. + */ +@Injectable() +export class AddonModQuizAccessDelayBetweenAttemptsHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessDelayBetweenAttempts'; + ruleName = 'quizaccess_delaybetweenattempts'; + + constructor() { + // Nothing to do. + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + return false; + } +} diff --git a/src/addon/mod/quiz/accessrules/ipaddress/ipaddress.module.ts b/src/addon/mod/quiz/accessrules/ipaddress/ipaddress.module.ts new file mode 100644 index 000000000..4a60d8c36 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/ipaddress/ipaddress.module.ts @@ -0,0 +1,30 @@ +// (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 { AddonModQuizAccessIpAddressHandler } from './providers/handler'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + ], + providers: [ + AddonModQuizAccessIpAddressHandler + ] +}) +export class AddonModQuizAccessIpAddressModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessIpAddressHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/accessrules/ipaddress/providers/handler.ts b/src/addon/mod/quiz/accessrules/ipaddress/providers/handler.ts new file mode 100644 index 000000000..ca2d1f3d0 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/ipaddress/providers/handler.ts @@ -0,0 +1,52 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; + +/** + * Handler to support IP address access rule. + */ +@Injectable() +export class AddonModQuizAccessIpAddressHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessIpAddress'; + ruleName = 'quizaccess_ipaddress'; + + constructor() { + // Nothing to do. + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + return false; + } +} diff --git a/src/addon/mod/quiz/accessrules/numattempts/numattempts.module.ts b/src/addon/mod/quiz/accessrules/numattempts/numattempts.module.ts new file mode 100644 index 000000000..ef194b2e8 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/numattempts/numattempts.module.ts @@ -0,0 +1,30 @@ +// (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 { AddonModQuizAccessNumAttemptsHandler } from './providers/handler'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + ], + providers: [ + AddonModQuizAccessNumAttemptsHandler + ] +}) +export class AddonModQuizAccessNumAttemptsModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessNumAttemptsHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/accessrules/numattempts/providers/handler.ts b/src/addon/mod/quiz/accessrules/numattempts/providers/handler.ts new file mode 100644 index 000000000..125bb2e8c --- /dev/null +++ b/src/addon/mod/quiz/accessrules/numattempts/providers/handler.ts @@ -0,0 +1,52 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; + +/** + * Handler to support num attempts access rule. + */ +@Injectable() +export class AddonModQuizAccessNumAttemptsHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessNumAttempts'; + ruleName = 'quizaccess_numattempts'; + + constructor() { + // Nothing to do. + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + return false; + } +} diff --git a/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.html b/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.html new file mode 100644 index 000000000..fad58a104 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.html @@ -0,0 +1,4 @@ + +

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

+

{{ 'addon.mod_quiz.confirmcontinueoffline' | translate:{$a: quiz.syncTimeReadable} }}

+
diff --git a/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.ts b/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.ts new file mode 100644 index 000000000..d23fd6927 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.ts @@ -0,0 +1,42 @@ +// (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, OnInit, Input } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; + +/** + * Component to render the preflight for offline attempts. + */ +@Component({ + selector: 'addon-mod-quiz-acess-offline-attempts', + templateUrl: 'offlineattempts.html' +}) +export class AddonModQuizAccessOfflineAttemptsComponent implements OnInit { + + @Input() quiz: any; // The quiz the rule belongs to. + @Input() attempt: any; // The attempt being started/continued. + @Input() prefetch: boolean; // Whether the user is prefetching the quiz. + @Input() siteId: string; // Site ID. + @Input() form: FormGroup; // Form where to add the form control. + + constructor(private fb: FormBuilder) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Always set confirmdatasaved to 1. Sending the data means the user accepted. + this.form.addControl('confirmdatasaved', this.fb.control(1)); + } +} diff --git a/src/addon/mod/quiz/accessrules/offlineattempts/offlineattempts.module.ts b/src/addon/mod/quiz/accessrules/offlineattempts/offlineattempts.module.ts new file mode 100644 index 000000000..5cd1adfcf --- /dev/null +++ b/src/addon/mod/quiz/accessrules/offlineattempts/offlineattempts.module.ts @@ -0,0 +1,46 @@ +// (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 { AddonModQuizAccessOfflineAttemptsHandler } from './providers/handler'; +import { AddonModQuizAccessOfflineAttemptsComponent } from './component/offlineattempts'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + AddonModQuizAccessOfflineAttemptsComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + AddonModQuizAccessOfflineAttemptsHandler + ], + exports: [ + AddonModQuizAccessOfflineAttemptsComponent + ], + entryComponents: [ + AddonModQuizAccessOfflineAttemptsComponent + ] +}) +export class AddonModQuizAccessOfflineAttemptsModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessOfflineAttemptsHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/accessrules/offlineattempts/providers/handler.ts b/src/addon/mod/quiz/accessrules/offlineattempts/providers/handler.ts new file mode 100644 index 000000000..3d5b38a24 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/offlineattempts/providers/handler.ts @@ -0,0 +1,91 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; +import { AddonModQuizSyncProvider } from '../../../providers/quiz-sync'; +import { AddonModQuizAccessOfflineAttemptsComponent } from '../component/offlineattempts'; + +/** + * Handler to support offline attempts access rule. + */ +@Injectable() +export class AddonModQuizAccessOfflineAttemptsHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessOfflineAttempts'; + ruleName = 'quizaccess_offlineattempts'; + + constructor() { + // Nothing to do. + } + + /** + * Add preflight data that doesn't require user interaction. The data should be added to the preflightData param. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} preflightData Object where to add the preflight data. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + getFixedPreflightData(quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise { + preflightData.confirmdatasaved = 1; + } + + /** + * Return the Component to use to display the access rule preflight. + * Implement this if your access rule requires a preflight check with user interaction. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getPreflightComponent(injector: Injector): any | Promise { + return AddonModQuizAccessOfflineAttemptsComponent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + if (prefetch) { + // Don't show the warning if the user is prefetching. + return false; + } + + if (!attempt) { + // User is starting a new attempt, show the warning. + return true; + } + + // Show warning if last sync was a while ago. + return Date.now() - AddonModQuizSyncProvider.SYNC_TIME > quiz.syncTime; + } +} diff --git a/src/addon/mod/quiz/accessrules/openclosedate/openclosedate.module.ts b/src/addon/mod/quiz/accessrules/openclosedate/openclosedate.module.ts new file mode 100644 index 000000000..3c23691b1 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/openclosedate/openclosedate.module.ts @@ -0,0 +1,30 @@ +// (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 { AddonModQuizAccessOpenCloseDateHandler } from './providers/handler'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + ], + providers: [ + AddonModQuizAccessOpenCloseDateHandler + ] +}) +export class AddonModQuizAccessOpenCloseDateModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessOpenCloseDateHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/accessrules/openclosedate/providers/handler.ts b/src/addon/mod/quiz/accessrules/openclosedate/providers/handler.ts new file mode 100644 index 000000000..b801b0e24 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/openclosedate/providers/handler.ts @@ -0,0 +1,75 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; +import { AddonModQuizProvider } from '../../../providers/quiz'; + +/** + * Handler to support open/close date access rule. + */ +@Injectable() +export class AddonModQuizAccessOpenCloseDateHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessOpenCloseDate'; + ruleName = 'quizaccess_openclosedate'; + + constructor() { + // Nothing to do. + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + return false; + } + + /** + * Whether or not the time left of an attempt should be displayed. + * + * @param {any} attempt The attempt. + * @param {number} endTime The attempt end time (in seconds). + * @param {number} timeNow The current time in seconds. + * @return {boolean} Whether it should be displayed. + */ + shouldShowTimeLeft(attempt: any, endTime: number, timeNow: number): boolean { + // If this is a teacher preview after the close date, do not show the time. + if (attempt.preview && timeNow > endTime) { + return false; + } + + // Show the time left only if it's less than QUIZ_SHOW_TIME_BEFORE_DEADLINE. + if (timeNow > endTime - AddonModQuizProvider.QUIZ_SHOW_TIME_BEFORE_DEADLINE) { + return true; + } + + return false; + } +} diff --git a/src/addon/mod/quiz/accessrules/password/component/password.html b/src/addon/mod/quiz/accessrules/password/component/password.html new file mode 100644 index 000000000..ec2a58f33 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/password/component/password.html @@ -0,0 +1,9 @@ + +

{{ 'addon.mod_quiz.quizpassword' | translate }}

+

{{ 'addon.mod_quiz.requirepasswordmessage' | translate}}

+
+ + + + + diff --git a/src/addon/mod/quiz/accessrules/password/component/password.ts b/src/addon/mod/quiz/accessrules/password/component/password.ts new file mode 100644 index 000000000..6205901e4 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/password/component/password.ts @@ -0,0 +1,42 @@ +// (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, OnInit, Input } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; + +/** + * Component to render the preflight for password. + */ +@Component({ + selector: 'addon-mod-quiz-acess-password', + templateUrl: 'password.html' +}) +export class AddonModQuizAccessPasswordComponent implements OnInit { + + @Input() quiz: any; // The quiz the rule belongs to. + @Input() attempt: any; // The attempt being started/continued. + @Input() prefetch: boolean; // Whether the user is prefetching the quiz. + @Input() siteId: string; // Site ID. + @Input() form: FormGroup; // Form where to add the form control. + + constructor(private fb: FormBuilder) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Add the control for the password. + this.form.addControl('quizpassword', this.fb.control('')); + } +} diff --git a/src/addon/mod/quiz/accessrules/password/password.module.ts b/src/addon/mod/quiz/accessrules/password/password.module.ts new file mode 100644 index 000000000..bfb5b6d95 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/password/password.module.ts @@ -0,0 +1,48 @@ +// (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 { AddonModQuizAccessPasswordHandler } from './providers/handler'; +import { AddonModQuizAccessPasswordComponent } from './component/password'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + AddonModQuizAccessPasswordComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule + ], + providers: [ + AddonModQuizAccessPasswordHandler + ], + exports: [ + AddonModQuizAccessPasswordComponent + ], + entryComponents: [ + AddonModQuizAccessPasswordComponent + ] +}) +export class AddonModQuizAccessPasswordModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessPasswordHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/accessrules/password/providers/handler.ts b/src/addon/mod/quiz/accessrules/password/providers/handler.ts new file mode 100644 index 000000000..7280b2e16 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/password/providers/handler.ts @@ -0,0 +1,201 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; +import { AddonModQuizAccessPasswordComponent } from '../component/password'; + +/** + * Handler to support password access rule. + */ +@Injectable() +export class AddonModQuizAccessPasswordHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessPassword'; + ruleName = 'quizaccess_password'; + + // Variables for database. + protected PASSWORD_TABLE = 'mod_quiz_access_password'; + protected tableSchema = { + name: this.PASSWORD_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'password', + type: 'TEXT' + }, + { + name: 'timemodified', + type: 'INTEGER' + } + ] + }; + + constructor(private sitesProvider: CoreSitesProvider) { + this.sitesProvider.createTableFromSchema(this.tableSchema); + } + + /** + * Add preflight data that doesn't require user interaction. The data should be added to the preflightData param. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} preflightData Object where to add the preflight data. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + getFixedPreflightData(quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise { + if (quiz && quiz.id && typeof preflightData.quizpassword == 'undefined') { + // Try to get a password stored. If it's found, use it. + return this.getPasswordEntry(quiz.id, siteId).then((entry) => { + preflightData.quizpassword = entry.password; + }).catch(() => { + // Don't reject. + }); + } + } + + /** + * Get a password stored in DB. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the DB entry on success. + */ + protected getPasswordEntry(quizId: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecord(this.PASSWORD_TABLE, {id: quizId}); + }); + } + + /** + * Return the Component to use to display the access rule preflight. + * Implement this if your access rule requires a preflight check with user interaction. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getPreflightComponent(injector: Injector): any | Promise { + return AddonModQuizAccessPasswordComponent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + // If there's a password stored don't require the preflight since we'll use the stored one. + return this.getPasswordEntry(quiz.id, siteId).then(() => { + return false; + }).catch(() => { + // Not stored. + return true; + }); + } + + /** + * Function called when the preflight check has passed. This is a chance to record that fact in some way. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} attempt The attempt started/continued. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + notifyPreflightCheckPassed(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : void | Promise { + + // The password is right, store it to use it automatically in following executions. + if (quiz && quiz.id && typeof preflightData.quizpassword != 'undefined') { + return this.storePassword(quiz.id, preflightData.quizpassword, siteId); + } + } + + /** + * Function called when the preflight check fails. This is a chance to record that fact in some way. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} attempt The attempt started/continued. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + notifyPreflightCheckFailed?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : void | Promise { + + // The password is wrong, remove it from DB if it's there. + if (quiz && quiz.id) { + return this.removePassword(quiz.id, siteId); + } + } + + /** + * Remove a password from DB. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + protected removePassword(quizId: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(this.PASSWORD_TABLE, {id: quizId}); + }); + } + + /** + * Store a password in DB. + * + * @param {number} quizId Quiz ID. + * @param {string} password Password. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + protected storePassword(quizId: number, password: string, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const entry = { + id: quizId, + password: password, + timemodified: Date.now() + }; + + return site.getDb().insertRecord(this.PASSWORD_TABLE, entry); + }); + } +} diff --git a/src/addon/mod/quiz/accessrules/safebrowser/providers/handler.ts b/src/addon/mod/quiz/accessrules/safebrowser/providers/handler.ts new file mode 100644 index 000000000..b5ec88157 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/safebrowser/providers/handler.ts @@ -0,0 +1,52 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; + +/** + * Handler to support safe address access rule. + */ +@Injectable() +export class AddonModQuizAccessSafeBrowserHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessSafeBrowser'; + ruleName = 'quizaccess_safebrowser'; + + constructor() { + // Nothing to do. + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + return false; + } +} diff --git a/src/addon/mod/quiz/accessrules/safebrowser/safebrowser.module.ts b/src/addon/mod/quiz/accessrules/safebrowser/safebrowser.module.ts new file mode 100644 index 000000000..6e326e8cf --- /dev/null +++ b/src/addon/mod/quiz/accessrules/safebrowser/safebrowser.module.ts @@ -0,0 +1,30 @@ +// (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 { AddonModQuizAccessSafeBrowserHandler } from './providers/handler'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + ], + providers: [ + AddonModQuizAccessSafeBrowserHandler + ] +}) +export class AddonModQuizAccessSafeBrowserModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessSafeBrowserHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/accessrules/securewindow/providers/handler.ts b/src/addon/mod/quiz/accessrules/securewindow/providers/handler.ts new file mode 100644 index 000000000..d5bb5334b --- /dev/null +++ b/src/addon/mod/quiz/accessrules/securewindow/providers/handler.ts @@ -0,0 +1,52 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; + +/** + * Handler to support secure window access rule. + */ +@Injectable() +export class AddonModQuizAccessSecureWindowHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessSecureWindow'; + ruleName = 'quizaccess_securewindow'; + + constructor() { + // Nothing to do. + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + return false; + } +} diff --git a/src/addon/mod/quiz/accessrules/securewindow/securewindow.module.ts b/src/addon/mod/quiz/accessrules/securewindow/securewindow.module.ts new file mode 100644 index 000000000..216f3b8b8 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/securewindow/securewindow.module.ts @@ -0,0 +1,30 @@ +// (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 { AddonModQuizAccessSecureWindowHandler } from './providers/handler'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + ], + providers: [ + AddonModQuizAccessSecureWindowHandler + ] +}) +export class AddonModQuizAccessSecureWindowModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessSecureWindowHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.html b/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.html new file mode 100644 index 000000000..7535b16b7 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.html @@ -0,0 +1,4 @@ + +

{{ 'addon.mod_quiz.confirmstartheader' | translate }}

+

{{ 'addon.mod_quiz.confirmstart' | translate:{$a: quiz.readableTimeLimit} }}

+
diff --git a/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.ts b/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.ts new file mode 100644 index 000000000..27d19d661 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.ts @@ -0,0 +1,36 @@ +// (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 } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +/** + * Component to render the preflight for time limit. + */ +@Component({ + selector: 'addon-mod-quiz-acess-time-limit', + templateUrl: 'timelimit.html' +}) +export class AddonModQuizAccessTimeLimitComponent { + + @Input() quiz: any; // The quiz the rule belongs to. + @Input() attempt: any; // The attempt being started/continued. + @Input() prefetch: boolean; // Whether the user is prefetching the quiz. + @Input() siteId: string; // Site ID. + @Input() form: FormGroup; // Form where to add the form control. + + constructor() { + // Nothing to do, we don't need to send anything for time limit. + } +} diff --git a/src/addon/mod/quiz/accessrules/timelimit/providers/handler.ts b/src/addon/mod/quiz/accessrules/timelimit/providers/handler.ts new file mode 100644 index 000000000..8ddc8083d --- /dev/null +++ b/src/addon/mod/quiz/accessrules/timelimit/providers/handler.ts @@ -0,0 +1,79 @@ + +// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate'; +import { AddonModQuizAccessTimeLimitComponent } from '../component/timelimit'; + +/** + * Handler to support time limit access rule. + */ +@Injectable() +export class AddonModQuizAccessTimeLimitHandler implements AddonModQuizAccessRuleHandler { + name = 'AddonModQuizAccessTimeLimit'; + ruleName = 'quizaccess_timelimit'; + + constructor() { + // Nothing to do. + } + + /** + * Return the Component to use to display the access rule preflight. + * Implement this if your access rule requires a preflight check with user interaction. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getPreflightComponent(injector: Injector): any | Promise { + return AddonModQuizAccessTimeLimitComponent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise { + // Warning only required if the attempt is not already started. + return !attempt; + } + + /** + * Whether or not the time left of an attempt should be displayed. + * + * @param {any} attempt The attempt. + * @param {number} endTime The attempt end time (in seconds). + * @param {number} timeNow The current time in seconds. + * @return {boolean} Whether it should be displayed. + */ + shouldShowTimeLeft(attempt: any, endTime: number, timeNow: number): boolean { + // If this is a teacher preview after the time limit expires, don't show the time left. + return !(attempt.preview && timeNow > endTime); + } +} diff --git a/src/addon/mod/quiz/accessrules/timelimit/timelimit.module.ts b/src/addon/mod/quiz/accessrules/timelimit/timelimit.module.ts new file mode 100644 index 000000000..850c2fb49 --- /dev/null +++ b/src/addon/mod/quiz/accessrules/timelimit/timelimit.module.ts @@ -0,0 +1,46 @@ +// (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 { AddonModQuizAccessTimeLimitHandler } from './providers/handler'; +import { AddonModQuizAccessTimeLimitComponent } from './component/timelimit'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + AddonModQuizAccessTimeLimitComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + AddonModQuizAccessTimeLimitHandler + ], + exports: [ + AddonModQuizAccessTimeLimitComponent + ], + entryComponents: [ + AddonModQuizAccessTimeLimitComponent + ] +}) +export class AddonModQuizAccessTimeLimitModule { + constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessTimeLimitHandler) { + accessRuleDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/quiz/classes/auto-save.ts b/src/addon/mod/quiz/classes/auto-save.ts new file mode 100644 index 000000000..9d31a9ab3 --- /dev/null +++ b/src/addon/mod/quiz/classes/auto-save.ts @@ -0,0 +1,227 @@ +// (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 { PopoverController, Popover } from 'ionic-angular'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { AddonModQuizProvider } from '../providers/quiz'; +import { AddonModQuizConnectionErrorComponent } from '../components/connection-error/connection-error'; +import { BehaviorSubject } from 'rxjs'; + +/** + * Class to support auto-save in quiz. Every certain seconds, it will check if there are changes in the current page answers + * and, if so, it will save them automatically. + */ +export class AddonModQuizAutoSave { + protected CHECK_CHANGES_INTERVAL = 5000; + + protected logger; + protected checkChangesInterval; // Interval to check if there are changes in the answers. + protected loadPreviousAnswersTimeout; // Timeout to load previous answers. + protected autoSaveTimeout; // Timeout to auto-save the answers. + protected popover: Popover; // Popover to display there's been an error. + protected popoverShown = false; // Whether the popover is shown. + protected previousAnswers: any; // The previous answers. It is used to check if answers have changed. + protected errorObservable: BehaviorSubject; // An observable to notify if there's been an error. + + /** + * Constructor. + * + * @param {string} formName Name of the form where the answers are stored. + * @param {string} buttonSelector Selector to find the button to show the connection error. + * @param {CoreLoggerProvider} loggerProvider CoreLoggerProvider instance. + * @param {PopoverController} popoverCtrl PopoverController instance. + * @param {CoreQuestionHelperProvider} questionHelper CoreQuestionHelperProvider instance. + * @param {AddonModQuizProvider} quizProvider AddonModQuizProvider instance. + */ + constructor(protected formName: string, protected buttonSelector: string, loggerProvider: CoreLoggerProvider, + protected popoverCtrl: PopoverController, protected questionHelper: CoreQuestionHelperProvider, + protected quizProvider: AddonModQuizProvider) { + + this.logger = loggerProvider.getInstance('AddonModQuizAutoSave'); + + // Create the popover. + this.popover = this.popoverCtrl.create(AddonModQuizConnectionErrorComponent); + this.popover.onDidDismiss(() => { + this.popoverShown = false; + }); + + // Create the observable to notify if an error happened. + this.errorObservable = new BehaviorSubject(false); + } + + /** + * Cancel a pending auto save. + */ + cancelAutoSave(): void { + clearTimeout(this.autoSaveTimeout); + this.autoSaveTimeout = undefined; + } + + /** + * Check if the answers have changed in a page. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight data. + * @param {boolean} [offline] Whether the quiz is being attempted in offline mode. + */ + checkChanges(quiz: any, attempt: any, preflightData: any, offline?: boolean): void { + if (this.autoSaveTimeout) { + // We already have an auto save pending, no need to check changes. + return; + } + + const answers = this.getAnswers(); + + if (!this.previousAnswers) { + // Previous answers isn't set, set it now. + this.previousAnswers = answers; + } else { + // Check if answers have changed. + let equal = true; + + for (const name in answers) { + if (this.previousAnswers[name] != answers[name]) { + equal = false; + break; + } + } + + if (!equal) { + this.setAutoSaveTimer(quiz, attempt, preflightData, offline); + } + + this.previousAnswers = answers; + } + } + + /** + * Get answers from a form. + * + * @return {any} Answers. + */ + protected getAnswers(): any { + return this.questionHelper.getAnswersFromForm(document.forms[this.formName]); + } + + /** + * Hide the auto save error. + */ + hideAutoSaveError(): void { + this.errorObservable.next(false); + this.popover.dismiss(); + } + + /** + * Returns an observable that will notify when an error happens or stops. + * It will send true when there's an error, and false when the error has been ammended. + * + * @return {BehaviorSubject} Observable. + */ + onError(): BehaviorSubject { + return this.errorObservable; + } + + /** + * Schedule an auto save process if it's not scheduled already. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight data. + * @param {boolean} [offline] Whether the quiz is being attempted in offline mode. + */ + setAutoSaveTimer(quiz: any, attempt: any, preflightData: any, offline?: boolean): void { + // Don't schedule if already shceduled or quiz is almost closed. + if (quiz.autosaveperiod && !this.autoSaveTimeout && !this.quizProvider.isAttemptTimeNearlyOver(quiz, attempt)) { + + // Schedule save. + this.autoSaveTimeout = setTimeout(() => { + const answers = this.getAnswers(); + this.cancelAutoSave(); + this.previousAnswers = answers; // Update previous answers to match what we're sending to the server. + + this.quizProvider.saveAttempt(quiz, attempt, answers, preflightData, offline).then(() => { + // Save successful, we can hide the connection error if it was shown. + this.hideAutoSaveError(); + }).catch((error) => { + // Error auto-saving. Show error and set timer again. + this.logger.warn('Error auto-saving data.', error); + + // If there was no error already, show the error message. + if (!this.errorObservable.getValue()) { + this.errorObservable.next(true); + this.showAutoSaveError(); + } + + // Try again. + this.setAutoSaveTimer(quiz, attempt, preflightData, offline); + }); + }, quiz.autosaveperiod * 1000); + } + } + + /** + * Show an error popover due to an auto save error. + */ + showAutoSaveError(ev?: Event): void { + // Don't show popover if it was already shown. + if (!this.popoverShown) { + this.popoverShown = true; + + // If no event is provided, simulate it targeting the button. + this.popover.present({ + ev: ev || { target: document.querySelector(this.buttonSelector) } + }); + } + } + + /** + * Start a process to periodically check changes in answers. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight data. + * @param {boolean} [offline] Whether the quiz is being attempted in offline mode. + */ + startCheckChangesProcess(quiz: any, attempt: any, preflightData: any, offline?: boolean): void { + if (this.checkChangesInterval || !quiz.autosaveperiod) { + // We already have the interval in place or the quiz has autosave disabled. + return; + } + + this.previousAnswers = undefined; + + // Load initial answers in 2.5 seconds so the first check interval finds them already loaded. + this.loadPreviousAnswersTimeout = setTimeout(() => { + this.checkChanges(quiz, attempt, preflightData, offline); + }, 2500); + + // Check changes every certain time. + this.checkChangesInterval = setInterval(() => { + this.checkChanges(quiz, attempt, preflightData, offline); + }, this.CHECK_CHANGES_INTERVAL); + } + + /** + * Stops the periodical check for changes. + */ + stopCheckChangesProcess(): void { + clearTimeout(this.loadPreviousAnswersTimeout); + clearInterval(this.checkChangesInterval); + + this.loadPreviousAnswersTimeout = undefined; + this.checkChangesInterval = undefined; + } +} diff --git a/src/addon/mod/quiz/components/components.module.ts b/src/addon/mod/quiz/components/components.module.ts new file mode 100644 index 000000000..e08c92cd4 --- /dev/null +++ b/src/addon/mod/quiz/components/components.module.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 { 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 { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { AddonModQuizIndexComponent } from './index/index'; +import { AddonModQuizConnectionErrorComponent } from './connection-error/connection-error'; + +@NgModule({ + declarations: [ + AddonModQuizIndexComponent, + AddonModQuizConnectionErrorComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule + ], + providers: [ + ], + exports: [ + AddonModQuizIndexComponent, + AddonModQuizConnectionErrorComponent + ], + entryComponents: [ + AddonModQuizIndexComponent, + AddonModQuizConnectionErrorComponent + ] +}) +export class AddonModQuizComponentsModule {} diff --git a/src/addon/mod/quiz/components/connection-error/connection-error.scss b/src/addon/mod/quiz/components/connection-error/connection-error.scss new file mode 100644 index 000000000..334d83ebf --- /dev/null +++ b/src/addon/mod/quiz/components/connection-error/connection-error.scss @@ -0,0 +1,7 @@ +addon-mod-quiz-connection-error { + background-color: $red-light; + + .item { + background-color: $red-light; + } +} diff --git a/src/addon/mod/quiz/components/connection-error/connection-error.ts b/src/addon/mod/quiz/components/connection-error/connection-error.ts new file mode 100644 index 000000000..e6013e240 --- /dev/null +++ b/src/addon/mod/quiz/components/connection-error/connection-error.ts @@ -0,0 +1,29 @@ +// (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'; + +/** + * Component that displays a quiz entry page. + */ +@Component({ + selector: 'addon-mod-quiz-connection-error', + template: '{{ "addon.mod_quiz.connectionerror" | translate }}', +}) +export class AddonModQuizConnectionErrorComponent { + + constructor() { + // Nothing to do. + } +} diff --git a/src/addon/mod/quiz/components/index/index.html b/src/addon/mod/quiz/components/index/index.html new file mode 100644 index 000000000..04a3b504b --- /dev/null +++ b/src/addon/mod/quiz/components/index/index.html @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + +

{{ rule }}

+
+ +

{{ 'addon.mod_quiz.grademethod' | translate }}

+

{{ quiz.gradeMethodReadable }}

+
+ +

{{ 'core.lastsync' | translate }}

+

{{ syncTime }}

+
+
+
+ + + + +

{{ 'addon.mod_quiz.summaryofattempts' | translate }}

+
+ + + + + {{ 'addon.mod_quiz.attemptnumber' | translate }} + # + {{ 'addon.mod_quiz.attemptstate' | translate }} + {{ 'addon.mod_quiz.marks' | translate }} / {{ quiz.sumGradesFormatted }} + {{ 'addon.mod_quiz.grade' | translate }} / {{ quiz.gradeFormatted }} + + + + + + {{ 'addon.mod_quiz.preview' | translate }} + {{ attempt.attempt }} + +

{{ sentence }}

+
+

{{ attempt.readableMark }}

+

{{ attempt.readableGrade }}

+
+
+
+
+ + + + + {{ gradeResult }} + {{ 'core.course.overriddennotice' | translate }} + +

{{ 'addon.mod_quiz.comment' | translate }}

+

+
+ +

{{ 'addon.mod_quiz.overallfeedback' | translate }}

+

+
+
+
+ + + + + + +

{{ message }}

+
+ +

{{ 'addon.mod_quiz.noquestions' | translate }}

+
+ +

{{ 'addon.mod_quiz.errorquestionsnotsupported' | translate }}

+

{{ type }}

+
+ +

{{ 'addon.mod_quiz.errorrulesnotsupported' | translate }}

+

{{ name }}

+
+ +

{{ 'addon.mod_quiz.errorbehaviournotsupported' | translate }}

+

{{ quiz.preferredbehaviour }}

+
+ + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ + + + + + + + + + {{ 'core.openinbrowser' | translate }} + + + + + + + + +
+
+
diff --git a/src/addon/mod/quiz/components/index/index.scss b/src/addon/mod/quiz/components/index/index.scss new file mode 100644 index 000000000..279d948b5 --- /dev/null +++ b/src/addon/mod/quiz/components/index/index.scss @@ -0,0 +1,34 @@ +addon-mod-quiz-index { + + .addon-mod_quiz-table { + .addon-mod_quiz-table-header .item-inner { + background-image: none; + font-size: 0.9em; + } + + .item-inner ion-label { + margin-right: 0; + } + + .item { + padding-left: 0; + } + + .label { + margin-top: 0; + margin-bottom: 0; + } + + .item:nth-child(even) { + background-color: $gray-lighter; + } + + .addon-mod_quiz-highlighted, + .item.addon-mod_quiz-highlighted, + .addon-mod_quiz-highlighted p, + .item.addon-mod_quiz-highlighted p { + background-color: $blue-light; + color: $blue-dark; + } + } +} diff --git a/src/addon/mod/quiz/components/index/index.ts b/src/addon/mod/quiz/components/index/index.ts new file mode 100644 index 000000000..59561a7c9 --- /dev/null +++ b/src/addon/mod/quiz/components/index/index.ts @@ -0,0 +1,607 @@ +// (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, Optional, Injector } from '@angular/core'; +import { Content, NavController } from 'ionic-angular'; +import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { AddonModQuizProvider } from '../../providers/quiz'; +import { AddonModQuizHelperProvider } from '../../providers/helper'; +import { AddonModQuizOfflineProvider } from '../../providers/quiz-offline'; +import { AddonModQuizSyncProvider } from '../../providers/quiz-sync'; +import { AddonModQuizPrefetchHandler } from '../../providers/prefetch-handler'; +import { CoreConstants } from '@core/constants'; + +/** + * Component that displays a quiz entry page. + */ +@Component({ + selector: 'addon-mod-quiz-index', + templateUrl: 'index.html', +}) +export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComponent { + component = AddonModQuizProvider.COMPONENT; + moduleName = 'quiz'; + + quiz: any; // The quiz. + now: number; // Current time. + syncTime: string; // Last synchronization time. + hasOffline: boolean; // Whether the quiz has offline data. + accessRules: string[]; // List of access rules of the quiz. + unsupportedRules: string[]; // List of unsupported access rules of the quiz. + unsupportedQuestions: string[]; // List of unsupported question types of the quiz. + behaviourSupported: boolean; // Whether the quiz behaviour is supported. + showResults: boolean; // Whether to show the result of the quiz (grade, etc.). + gradeOverridden: boolean; // Whether grade has been overridden. + gradebookFeedback: string; // The feedback in the gradebook. + gradeResult: string; // Message with the grade. + overallFeedback: string; // The feedback for the grade. + buttonText: string; // Text to display in the start/continue button. + preventMessages: string[]; // List of messages explaining why the quiz cannot be attempted. + showStatusSpinner = true; // Whether to show a spinner due to quiz status. + + protected fetchContentDefaultError = 'addon.mod_quiz.errorgetquiz'; // Default error to show when loading contents. + protected syncEventName = AddonModQuizSyncProvider.AUTO_SYNCED; + + protected quizData: any; // Quiz instance. This variable will store the quiz instance until it's ready to be shown + protected autoReview: any; // Data to auto-review an attempt. It's used to automatically open the review page after finishing. + protected quizAccessInfo: any; // Quiz access info. + protected attemptAccessInfo: any; // Last attempt access info. + protected attempts: any[]; // List of attempts the user has made. + protected moreAttempts: boolean; // Whether user can create/continue attempts. + protected options: any; // Combined review options. + protected bestGrade: any; // Best grade data. + protected gradebookData: {grade: number, feedback?: string}; // The gradebook grade and feedback. + protected overallStats: boolean; // Equivalent to overallstats in mod_quiz_view_object in Moodle. + protected finishedObserver: any; // It will observe attempt finished events. + protected hasPlayed = false; // Whether the user has gone to the quiz player (attempted). + + constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() protected content: Content, + protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider, + protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate, + protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController, + protected prefetchDelegate: CoreCourseModulePrefetchDelegate) { + super(injector); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.loadContent(false, true).then(() => { + if (!this.quizData) { + return; + } + + this.quizProvider.logViewQuiz(this.quizData.id).then(() => { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }).catch((error) => { + // Ignore errors. + }); + }); + + // Listen for attempt finished events. + this.finishedObserver = this.eventsProvider.on(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, (data) => { + // Go to review attempt if an attempt in this quiz was finished and synced. + if (this.quizData && data.quizId == this.quizData.id) { + this.autoReview = data; + } + }, this.siteId); + } + + /** + * Attempt the quiz. + */ + attemptQuiz(): void { + if (this.showStatusSpinner) { + // Quiz is being downloaded or synchronized, abort. + return; + } + + if (this.quizProvider.isQuizOffline(this.quizData)) { + // Quiz supports offline, check if it needs to be downloaded. + // If the site doesn't support check updates, always prefetch it because we cannot tell if there's something new. + const isDownloaded = this.currentStatus == CoreConstants.DOWNLOADED; + + if (!isDownloaded || !this.prefetchDelegate.canCheckUpdates()) { + // Prefetch the quiz. + this.showStatusSpinner = true; + + this.prefetchHandler.prefetch(this.module, this.courseId, true).then(() => { + // Success downloading, open quiz. + this.openQuiz(); + }).catch((error) => { + if (this.hasOffline || (isDownloaded && !this.prefetchDelegate.canCheckUpdates())) { + // Error downloading but there is something offline, allow continuing it. + // If the site doesn't support check updates, continue too because we cannot tell if there's something new. + this.openQuiz(); + } else { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + } + + }).finally(() => { + this.showStatusSpinner = false; + }); + } else { + // Already downloaded, open it. + this.openQuiz(); + } + } else { + // Quiz isn't offline, just open it. + this.openQuiz(); + } + } + + /** + * Get the quiz data. + * + * @param {boolean} [refresh=false] If it's refreshing content. + * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + + // First get the quiz instance. + return this.quizProvider.getQuiz(this.courseId, this.module.id).then((quizData) => { + this.quizData = quizData; + this.quizData.gradeMethodReadable = this.quizProvider.getQuizGradeMethod(this.quizData.grademethod); + + this.now = new Date().getTime(); + this.dataRetrieved.emit(this.quizData); + this.description = this.quizData.intro || this.description; + + // Try to get warnings from automatic sync. + return this.quizSync.getSyncWarnings(this.quizData.id).then((warnings) => { + if (warnings && warnings.length) { + // Show warnings and delete them so they aren't shown again. + this.domUtils.showErrorModal(this.textUtils.buildMessage(warnings)); + + return this.quizSync.setSyncWarnings(this.quizData.id, []); + } + }); + }).then(() => { + if (this.quizProvider.isQuizOffline(this.quizData)) { + // Try to sync the quiz. + return this.syncActivity(showErrors).catch(() => { + // Ignore errors, keep getting data even if sync fails. + this.autoReview = undefined; + }); + } else { + this.autoReview = undefined; + this.showStatusSpinner = false; + } + }).then(() => { + + if (this.quizProvider.isQuizOffline(this.quizData)) { + // Handle status. + this.setStatusListener(); + + // Get last synchronization time and check if sync button should be seen. + // No need to return these promises, they should be faster than the rest. + this.quizSync.getReadableSyncTime(this.quizData.id).then((syncTime) => { + this.syncTime = syncTime; + }); + + this.quizSync.hasDataToSync(this.quizData.id).then((hasOffline) => { + this.hasOffline = hasOffline; + }); + } + + // Get quiz access info. + return this.quizProvider.getQuizAccessInformation(this.quizData.id).then((info) => { + this.quizAccessInfo = info; + this.quizData.showReviewColumn = info.canreviewmyattempts; + this.accessRules = info.accessrules; + this.unsupportedRules = this.quizProvider.getUnsupportedRules(info.activerulenames); + + if (this.quizData.preferredbehaviour) { + this.behaviourSupported = this.behaviourDelegate.isBehaviourSupported(this.quizData.preferredbehaviour); + } + + // Get question types in the quiz. + return this.quizProvider.getQuizRequiredQtypes(this.quizData.id).then((types) => { + this.unsupportedQuestions = this.quizProvider.getUnsupportedQuestions(types); + + return this.getAttempts(); + }); + }); + + }).then(() => { + // All data obtained, now fill the context menu. + this.fillContextMenu(refresh); + + // Quiz is ready to be shown, move it to the variable that is displayed. + this.quiz = this.quizData; + }); + } + + /** + * Get the user attempts in the quiz and the result info. + * + * @return {Promise} Promise resolved when done. + */ + protected getAttempts(): Promise { + + // Get access information of last attempt (it also works if no attempts made). + return this.quizProvider.getAttemptAccessInformation(this.quizData.id, 0).then((info) => { + this.attemptAccessInfo = info; + + // Get attempts. + return this.quizProvider.getUserAttempts(this.quizData.id).then((atts) => { + + return this.treatAttempts(atts).then((atts) => { + this.attempts = atts; + + // Check if user can create/continue attempts. + if (this.attempts.length) { + const last = this.attempts[this.attempts.length - 1]; + this.moreAttempts = !this.quizProvider.isAttemptFinished(last.state) || !this.attemptAccessInfo.isfinished; + } else { + this.moreAttempts = !this.attemptAccessInfo.isfinished; + } + + this.getButtonText(); + + return this.getResultInfo(); + }); + }); + }); + } + + /** + * Get the text to show in the button. It also sets restriction messages if needed. + */ + protected getButtonText(): void { + this.buttonText = ''; + + if (this.quizData.hasquestions !== 0) { + if (this.attempts.length && !this.quizProvider.isAttemptFinished(this.attempts[this.attempts.length - 1].state)) { + // Last attempt is unfinished. + if (this.quizAccessInfo.canattempt) { + this.buttonText = 'addon.mod_quiz.continueattemptquiz'; + } else if (this.quizAccessInfo.canpreview) { + this.buttonText = 'addon.mod_quiz.continuepreview'; + } + + } else { + // Last attempt is finished or no attempts. + if (this.quizAccessInfo.canattempt) { + this.preventMessages = this.attemptAccessInfo.preventnewattemptreasons; + if (!this.preventMessages.length) { + if (!this.attempts.length) { + this.buttonText = 'addon.mod_quiz.attemptquiznow'; + } else { + this.buttonText = 'addon.mod_quiz.reattemptquiz'; + } + } + } else if (this.quizAccessInfo.canpreview) { + this.buttonText = 'addon.mod_quiz.previewquiznow'; + } + } + } + + if (this.buttonText) { + // So far we think a button should be printed, check if they will be allowed to access it. + this.preventMessages = this.quizAccessInfo.preventaccessreasons; + + if (!this.moreAttempts) { + this.buttonText = ''; + } else if (this.quizAccessInfo.canattempt && this.preventMessages.length) { + this.buttonText = ''; + } else if (this.unsupportedQuestions.length || this.unsupportedRules.length || !this.behaviourSupported) { + this.buttonText = ''; + } + } + } + + /** + * Get result info to show. + * + * @return {Promise} Promise resolved when done. + */ + protected getResultInfo(): Promise { + + if (this.attempts.length && this.quizData.showGradeColumn && this.bestGrade.hasgrade && + typeof this.gradebookData.grade != 'undefined') { + + const formattedGradebookGrade = this.quizProvider.formatGrade(this.gradebookData.grade, this.quizData.decimalpoints), + formattedBestGrade = this.quizProvider.formatGrade(this.bestGrade.grade, this.quizData.decimalpoints); + let gradeToShow = formattedGradebookGrade; // By default we show the grade in the gradebook. + + this.showResults = true; + this.gradeOverridden = formattedGradebookGrade != formattedBestGrade; + this.gradebookFeedback = this.gradebookData.feedback; + + if (this.bestGrade.grade > this.gradebookData.grade && this.gradebookData.grade == this.quizData.grade) { + // The best grade is higher than the max grade for the quiz. + // We'll do like Moodle web and show the best grade instead of the gradebook grade. + this.gradeOverridden = false; + gradeToShow = formattedBestGrade; + } + + if (this.overallStats) { + // Show the quiz grade. The message shown is different if the quiz is finished. + if (this.moreAttempts) { + this.gradeResult = this.translate.instant('addon.mod_quiz.gradesofar', {$a: { + method: this.quizData.gradeMethodReadable, + mygrade: gradeToShow, + quizgrade: this.quizData.gradeFormatted + }}); + } else { + const outOfShort = this.translate.instant('addon.mod_quiz.outofshort', {$a: { + grade: gradeToShow, + maxgrade: this.quizData.gradeFormatted + }}); + + this.gradeResult = this.translate.instant('addon.mod_quiz.yourfinalgradeis', {$a: outOfShort}); + } + } + + if (this.quizData.showFeedbackColumn) { + // Get the quiz overall feedback. + return this.quizProvider.getFeedbackForGrade(this.quizData.id, this.gradebookData.grade).then((response) => { + this.overallFeedback = response.feedbacktext; + }); + } + } else { + this.showResults = false; + } + + return Promise.resolve(); + } + + /** + * Go to review an attempt that has just been finished. + * + * @return {Promise} Promise resolved when done. + */ + protected goToAutoReview(): Promise { + // If we go to auto review it means an attempt was finished. Check completion status. + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + + // Verify that user can see the review. + const attemptId = this.autoReview.attemptId; + + if (this.quizAccessInfo.canreviewmyattempts) { + return this.quizProvider.getAttemptReview(attemptId, -1).then(() => { + this.navCtrl.push('AddonModQuizReviewPage', {courseId: this.courseId, quizId: this.quizData.id, attemptId}); + }).catch(() => { + // Ignore errors. + }); + } + + return Promise.resolve(); + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param {any} result Data returned on the sync function. + * @return {boolean} If suceed or not. + */ + protected hasSyncSucceed(result: any): boolean { + if (result.attemptFinished) { + // An attempt was finished, check completion status. + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + } + + // If the sync call isn't rejected it means the sync was successful. + return result.answersSent; + } + + /** + * User entered the page that contains the component. + */ + ionViewDidEnter(): void { + super.ionViewDidEnter(); + + if (this.hasPlayed) { + this.hasPlayed = false; + + // Update data when we come back from the player since the attempt status could have changed. + let promise; + + // Check if we need to go to review an attempt automatically. + if (this.autoReview && this.autoReview.synced) { + promise = this.goToAutoReview(); + this.autoReview = undefined; + } else { + promise = Promise.resolve(); + } + + // Refresh data. + this.loaded = false; + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + this.content.scrollToTop(); + + promise.then(() => { + this.refreshContent().finally(() => { + this.loaded = true; + this.refreshIcon = 'refresh'; + this.syncIcon = 'sync'; + }); + }); + } else { + this.autoReview = undefined; + } + } + + /** + * User left the page that contains the component. + */ + ionViewDidLeave(): void { + super.ionViewDidLeave(); + this.autoReview = undefined; + + if (this.navCtrl.getActive().component.name == 'AddonModQuizPlayerPage') { + this.hasPlayed = true; + } + } + + /** + * Perform the invalidate content function. + * + * @return {Promise} Resolved when done. + */ + protected invalidateContent(): Promise { + const promises = []; + + promises.push(this.quizProvider.invalidateQuizData(this.courseId)); + + if (this.quizData) { + promises.push(this.quizProvider.invalidateUserAttemptsForUser(this.quizData.id)); + promises.push(this.quizProvider.invalidateQuizAccessInformation(this.quizData.id)); + promises.push(this.quizProvider.invalidateQuizRequiredQtypes(this.quizData.id)); + promises.push(this.quizProvider.invalidateAttemptAccessInformation(this.quizData.id)); + promises.push(this.quizProvider.invalidateCombinedReviewOptionsForUser(this.quizData.id)); + promises.push(this.quizProvider.invalidateUserBestGradeForUser(this.quizData.id)); + promises.push(this.quizProvider.invalidateGradeFromGradebook(this.courseId)); + } + + return Promise.all(promises); + } + + /** + * Compares sync event data with current data to check if refresh content is needed. + * + * @param {any} syncEventData Data receiven on sync observer. + * @return {boolean} True if refresh is needed, false otherwise. + */ + protected isRefreshSyncNeeded(syncEventData: any): boolean { + if (syncEventData.attemptFinished) { + // An attempt was finished, check completion status. + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + } + + if (this.quizData && syncEventData.quizId == this.quizData.id) { + this.content.scrollToTop(); + + return true; + } + + return false; + } + + /** + * Open a quiz to attempt it. + */ + protected openQuiz(): void { + this.navCtrl.push('AddonModQuizPlayerPage', {courseId: this.courseId, quizId: this.quiz.id, moduleUrl: this.module.url}); + } + + /** + * Displays some data based on the current status. + * + * @param {string} status The current status. + * @param {string} [previousStatus] The previous status. If not defined, there is no previous status. + */ + protected showStatus(status: string, previousStatus?: string): void { + this.showStatusSpinner = status == CoreConstants.DOWNLOADING; + } + + /** + * Performs the sync of the activity. + * + * @return {Promise} Promise resolved when done. + */ + protected sync(): Promise { + return this.quizSync.syncQuiz(this.quizData, true); + } + + /** + * Treat user attempts. + * + * @param {any} attempts The attempts to treat. + * @return {Promise} Promise resolved when done. + */ + protected treatAttempts(attempts: any): Promise { + if (!attempts || !attempts.length) { + // There are no attempts to treat. + return Promise.resolve(attempts); + } + + const lastFinished = this.quizProvider.getLastFinishedAttemptFromList(attempts), + promises = []; + + if (this.autoReview && lastFinished && lastFinished.id >= this.autoReview.attemptId) { + // User just finished an attempt in offline and it seems it's been synced, since it's finished in online. + // Go to the review of this attempt if the user hasn't left this view. + if (!this.isDestroyed && this.isCurrentView) { + promises.push(this.goToAutoReview()); + } + this.autoReview = undefined; + } + + // Load flag to show if attempts are finished but not synced. + promises.push(this.quizProvider.loadFinishedOfflineData(attempts)); + + // Get combined review options. + promises.push(this.quizProvider.getCombinedReviewOptions(this.quizData.id).then((result) => { + this.options = result; + })); + + // Get best grade. + promises.push(this.quizProvider.getUserBestGrade(this.quizData.id).then((best) => { + this.bestGrade = best; + + // Get gradebook grade. + return this.quizProvider.getGradeFromGradebook(this.courseId, this.module.id).then((data) => { + this.gradebookData = { + grade: data.graderaw, + feedback: data.feedback + }; + }).catch(() => { + // Fallback to quiz best grade if failure or not found. + this.gradebookData = { + grade: this.bestGrade.grade + }; + }); + })); + + return Promise.all(promises).then(() => { + const grade: number = typeof this.gradebookData.grade != 'undefined' ? this.gradebookData.grade : this.bestGrade.grade, + quizGrade = this.quizProvider.formatGrade(grade, this.quizData.decimalpoints); + + // Calculate data to construct the header of the attempts table. + this.quizHelper.setQuizCalculatedData(this.quizData, this.options); + + this.overallStats = lastFinished && this.options.alloptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX; + + // Calculate data to show for each attempt. + attempts.forEach((attempt) => { + // Highlight the highest grade if appropriate. + const shouldHighlight = this.overallStats && this.quizData.grademethod == AddonModQuizProvider.GRADEHIGHEST && + attempts.length > 1; + + this.quizHelper.setAttemptCalculatedData(this.quizData, attempt, shouldHighlight, quizGrade); + }); + + return attempts; + }); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.finishedObserver && this.finishedObserver.off(); + } +} diff --git a/src/addon/mod/quiz/lang/en.json b/src/addon/mod/quiz/lang/en.json new file mode 100644 index 000000000..9cb5add1e --- /dev/null +++ b/src/addon/mod/quiz/lang/en.json @@ -0,0 +1,79 @@ +{ + "attemptfirst": "First attempt", + "attemptlast": "Last attempt", + "attemptnumber": "Attempt", + "attemptquiznow": "Attempt quiz now", + "attemptstate": "State", + "cannotsubmitquizdueto": "This quiz attempt cannot be submitted for the following reasons:", + "comment": "Comment", + "completedon": "Completed on", + "confirmclose": "Once you submit, you will no longer be able to change your answers for this attempt.", + "confirmcontinueoffline": "This attempt has not been synchronised since {{$a}}. If you have continued this attempt in another device since then, you may lose data.", + "confirmleavequizonerror": "An error occurred while saving the answers. Are you sure you want to leave the quiz?", + "confirmstart": "The quiz has a time limit of {{$a}}. Time will count down from the moment you start your attempt and you must submit before it expires. Are you sure that you wish to start now?", + "confirmstartheader": "Timed quiz", + "connectionerror": "Network connection lost. (Autosave failed).\n\nMake a note of any responses entered on this page in the last few minutes, then try to re-connect.\n\nOnce connection has been re-established, your responses should be saved and this message will disappear.", + "continueattemptquiz": "Continue the last attempt", + "continuepreview": "Continue the last preview", + "errorbehaviournotsupported": "This quiz can't be attempted in the app because the question behaviour is not supported by the app:", + "errordownloading": "Error downloading required data.", + "errorgetattempt": "Error getting attempt data.", + "errorgetquestions": "Error getting questions.", + "errorgetquiz": "Error getting quiz data.", + "errorparsequestions": "An error occurred while reading the questions. Please attempt this quiz in a web browser.", + "errorquestionsnotsupported": "This quiz can't be attempted in the app because it contains questions not supported by the app:", + "errorrulesnotsupported": "This quiz can't be attempted in the app because it has access rules not supported by the app:", + "errorsaveattempt": "An error occurred while saving the attempt data.", + "feedback": "Feedback", + "finishattemptdots": "Finish attempt...", + "finishnotsynced": "Finished but not synchronised", + "grade": "Grade", + "gradeaverage": "Average grade", + "gradehighest": "Highest grade", + "grademethod": "Grading method", + "gradesofar": "{{$a.method}}: {{$a.mygrade}} / {{$a.quizgrade}}.", + "hasdatatosync": "This quiz has offline data to be synchronised.", + "marks": "Marks", + "mustbesubmittedby": "This attempt must be submitted by {{$a}}.", + "noquestions": "No questions have been added yet", + "noreviewattempt": "You are not allowed to review this attempt.", + "notyetgraded": "Not yet graded", + "opentoc": "Open navigation popover", + "outof": "{{$a.grade}} out of {{$a.maxgrade}}", + "outofpercent": "{{$a.grade}} out of {{$a.maxgrade}} ({{$a.percent}}%)", + "outofshort": "{{$a.grade}}/{{$a.maxgrade}}", + "overallfeedback": "Overall feedback", + "overdue": "Overdue", + "overduemustbesubmittedby": "This attempt is now overdue. It should already have been submitted. If you would like this quiz to be graded, you must submit it by {{$a}}. If you do not submit it by then, no marks from this attempt will be counted.", + "preview": "Preview", + "previewquiznow": "Preview quiz now", + "question": "Question", + "quiznavigation": "Quiz navigation", + "quizpassword": "Quiz password", + "reattemptquiz": "Re-attempt quiz", + "requirepasswordmessage": "To attempt this quiz you need to know the quiz password", + "returnattempt": "Return to attempt", + "review": "Review", + "reviewofattempt": "Review of attempt {{$a}}", + "reviewofpreview": "Review of preview", + "showall": "Show all questions on one page", + "showeachpage": "Show one page at a time", + "startattempt": "Start attempt", + "startedon": "Started on", + "stateabandoned": "Never submitted", + "statefinished": "Finished", + "statefinisheddetails": "Submitted {{$a}}", + "stateinprogress": "In progress", + "stateoverdue": "Overdue", + "stateoverduedetails": "Must be submitted by {{$a}}", + "status": "Status", + "submitallandfinish": "Submit all and finish", + "summaryofattempt": "Summary of attempt", + "summaryofattempts": "Summary of your previous attempts", + "timeleft": "Time left", + "timetaken": "Time taken", + "warningattemptfinished": "Offline attempt discarded as it was finished on the site or not found.", + "warningdatadiscarded": "Some offline answers were discarded because the questions were modified online.", + "warningdatadiscardedfromfinished": "Attempt unfinished because some offline answers were discarded. Please review your answers then resubmit the attempt.", + "yourfinalgradeis": "Your final grade for this quiz is {{$a}}." +} \ No newline at end of file diff --git a/src/addon/mod/quiz/pages/attempt/attempt.html b/src/addon/mod/quiz/pages/attempt/attempt.html new file mode 100644 index 000000000..0150bb667 --- /dev/null +++ b/src/addon/mod/quiz/pages/attempt/attempt.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + +

{{ 'addon.mod_quiz.attemptnumber' | translate }}

+

{{ 'addon.mod_quiz.preview' | translate }}

+

{{ attempt.attempt }}

+
+ +

{{ 'addon.mod_quiz.attemptstate' | translate }}

+

{{ sentence }}

+
+ +

{{ 'addon.mod_quiz.marks' | translate }} / {{ quiz.sumGradesFormatted }}

+

{{ attempt.readableMark }}

+
+ +

{{ 'addon.mod_quiz.grade' | translate }} / {{ quiz.gradeFormatted }}

+

{{ attempt.readableGrade }}

+
+ +

{{ 'addon.mod_quiz.feedback' | translate }}

+

+
+ + + + +

{{ 'addon.mod_quiz.noreviewattempt' | translate }}

+
+
+
+
diff --git a/src/addon/mod/quiz/pages/attempt/attempt.module.ts b/src/addon/mod/quiz/pages/attempt/attempt.module.ts new file mode 100644 index 000000000..66257ff1b --- /dev/null +++ b/src/addon/mod/quiz/pages/attempt/attempt.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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModQuizAttemptPage } from './attempt'; + +@NgModule({ + declarations: [ + AddonModQuizAttemptPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(AddonModQuizAttemptPage), + TranslateModule.forChild() + ], +}) +export class AddonModQuizAttemptPageModule {} diff --git a/src/addon/mod/quiz/pages/attempt/attempt.ts b/src/addon/mod/quiz/pages/attempt/attempt.ts new file mode 100644 index 000000000..ed6abe247 --- /dev/null +++ b/src/addon/mod/quiz/pages/attempt/attempt.ts @@ -0,0 +1,177 @@ +// (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, OnInit } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModQuizProvider } from '../../providers/quiz'; +import { AddonModQuizHelperProvider } from '../../providers/helper'; + +/** + * Page that displays some summary data about an attempt. + */ +@IonicPage({ segment: 'addon-mod-quiz-attempt' }) +@Component({ + selector: 'page-addon-mod-quiz-attempt', + templateUrl: 'attempt.html', +}) +export class AddonModQuizAttemptPage implements OnInit { + courseId: number; // The course ID the quiz belongs to. + quiz: any; // The quiz the attempt belongs to. + attempt: any; // The attempt to view. + component = AddonModQuizProvider.COMPONENT; // Component to link the files to. + componentId: number; // Component ID to use in conjunction with the component. + loaded: boolean; // Whether data has been loaded. + + protected attemptId: number; // Attempt to view. + protected quizId: number; // ID of the quiz the attempt belongs to. + + constructor(navParams: NavParams, protected domUtils: CoreDomUtilsProvider, protected quizProvider: AddonModQuizProvider, + protected quizHelper: AddonModQuizHelperProvider) { + + this.attemptId = navParams.get('attemptId'); + this.quizId = navParams.get('quizId'); + this.courseId = navParams.get('courseId'); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchQuizData().finally(() => { + this.loaded = true; + }); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + doRefresh(refresher: any): void { + this.refreshData().finally(() => { + refresher.complete(); + }); + } + + /** + * Get quiz data and attempt data. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchQuizData(): Promise { + return this.quizProvider.getQuizById(this.courseId, this.quizId).then((quizData) => { + this.quiz = quizData; + this.componentId = this.quiz.coursemodule; + + return this.fetchAttempt(); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'addon.mod_quiz.errorgetattempt', true); + }); + } + + /** + * Get the attempt data. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchAttempt(): Promise { + const promises = []; + let options, + accessInfo; + + // Get all the attempts and search the one we want. + promises.push(this.quizProvider.getUserAttempts(this.quizId).then((attempts) => { + for (let i = 0; i < attempts.length; i++) { + const attempt = attempts[i]; + if (attempt.id == this.attemptId) { + this.attempt = attempt; + break; + } + } + + if (!this.attempt) { + // Attempt not found, error. + return Promise.reject(null); + } + + // Load flag to show if attempt is finished but not synced. + return this.quizProvider.loadFinishedOfflineData([this.attempt]); + })); + + promises.push(this.quizProvider.getCombinedReviewOptions(this.quiz.id).then((opts) => { + options = opts; + })); + + // Check if the user can review the attempt. + promises.push(this.quizProvider.getQuizAccessInformation(this.quiz.id).then((quizAccessInfo) => { + accessInfo = quizAccessInfo; + + if (accessInfo.canreviewmyattempts) { + return this.quizProvider.getAttemptReview(this.attemptId, -1).catch(() => { + // Error getting the review, assume the user cannot review the attempt. + accessInfo.canreviewmyattempts = false; + }); + } + })); + + return Promise.all(promises).then(() => { + + // Determine fields to show. + this.quizHelper.setQuizCalculatedData(this.quiz, options); + this.quiz.showReviewColumn = accessInfo.canreviewmyattempts; + + // Get readable data for the attempt. + this.quizHelper.setAttemptCalculatedData(this.quiz, this.attempt, false); + + // Check if the feedback should be displayed. + const grade = Number(this.attempt.rescaledGrade); + if (this.quiz.showFeedbackColumn && this.quizProvider.isAttemptFinished(this.attempt.state) && + options.someoptions.overallfeedback && !isNaN(grade)) { + + // Feedback should be displayed, get the feedback for the grade. + return this.quizProvider.getFeedbackForGrade(this.quiz.id, grade).then((response) => { + this.attempt.feedback = response.feedbacktext; + }); + } else { + delete this.attempt.feedback; + } + }); + } + + /** + * Refresh the data. + * + * @return {Promise} Promise resolved when done. + */ + protected refreshData(): Promise { + const promises = []; + + promises.push(this.quizProvider.invalidateQuizData(this.courseId)); + promises.push(this.quizProvider.invalidateUserAttemptsForUser(this.quizId)); + promises.push(this.quizProvider.invalidateQuizAccessInformation(this.quizId)); + promises.push(this.quizProvider.invalidateCombinedReviewOptionsForUser(this.quizId)); + promises.push(this.quizProvider.invalidateAttemptReview(this.attemptId)); + + if (this.attempt && typeof this.attempt.feedback != 'undefined') { + promises.push(this.quizProvider.invalidateFeedback(this.quizId)); + } + + return Promise.all(promises).catch(() => { + // Ignore errors. + }).then(() => { + return this.fetchQuizData(); + }); + } +} diff --git a/src/addon/mod/quiz/pages/index/index.html b/src/addon/mod/quiz/pages/index/index.html new file mode 100644 index 000000000..e8cebbc02 --- /dev/null +++ b/src/addon/mod/quiz/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/quiz/pages/index/index.module.ts b/src/addon/mod/quiz/pages/index/index.module.ts new file mode 100644 index 000000000..3ee1e482e --- /dev/null +++ b/src/addon/mod/quiz/pages/index/index.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 { AddonModQuizComponentsModule } from '../../components/components.module'; +import { AddonModQuizIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModQuizIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModQuizComponentsModule, + IonicPageModule.forChild(AddonModQuizIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModQuizIndexPageModule {} diff --git a/src/addon/mod/quiz/pages/index/index.ts b/src/addon/mod/quiz/pages/index/index.ts new file mode 100644 index 000000000..be398552e --- /dev/null +++ b/src/addon/mod/quiz/pages/index/index.ts @@ -0,0 +1,62 @@ +// (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 { AddonModQuizIndexComponent } from '../../components/index/index'; + +/** + * Page that displays the quiz entry page. + */ +@IonicPage({ segment: 'addon-mod-quiz-index' }) +@Component({ + selector: 'page-addon-mod-quiz-index', + templateUrl: 'index.html', +}) +export class AddonModQuizIndexPage { + @ViewChild(AddonModQuizIndexComponent) quizComponent: AddonModQuizIndexComponent; + + title: string; + module: any; + courseId: number; + + constructor(navParams: NavParams) { + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.title = this.module.name; + } + + /** + * Update some data based on the quiz instance. + * + * @param {any} quiz Quiz instance. + */ + updateData(quiz: any): void { + this.title = quiz.name || this.title; + } + + /** + * User entered the page. + */ + ionViewDidEnter(): void { + this.quizComponent.ionViewDidEnter(); + } + + /** + * User left the page. + */ + ionViewDidLeave(): void { + this.quizComponent.ionViewDidLeave(); + } +} diff --git a/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.html b/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.html new file mode 100644 index 000000000..c7c17aefd --- /dev/null +++ b/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.html @@ -0,0 +1,41 @@ + + + {{ 'addon.mod_quiz.quiznavigation' | translate }} + + + + + + + + diff --git a/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.module.ts b/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.module.ts new file mode 100644 index 000000000..51d29607a --- /dev/null +++ b/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.module.ts @@ -0,0 +1,29 @@ +// (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 { AddonModQuizNavigationModalPage } from './navigation-modal'; +import { TranslateModule } from '@ngx-translate/core'; + +@NgModule({ + declarations: [ + AddonModQuizNavigationModalPage + ], + imports: [ + IonicPageModule.forChild(AddonModQuizNavigationModalPage), + TranslateModule.forChild() + ] +}) +export class AddonModQuizNavigationModalPageModule {} diff --git a/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.scss b/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.scss new file mode 100644 index 000000000..5abb5c103 --- /dev/null +++ b/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.scss @@ -0,0 +1,5 @@ +page-addon-mod-quiz-navigation-modal { + .addon-mod_quiz-selected, .item.addon-mod_quiz-selected { + background: $blue-light; + } +} diff --git a/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.ts b/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.ts new file mode 100644 index 000000000..a01013fed --- /dev/null +++ b/src/addon/mod/quiz/pages/navigation-modal/navigation-modal.ts @@ -0,0 +1,69 @@ +// (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, ViewController, NavParams } from 'ionic-angular'; + +/** + * Modal that renders the quiz navigation. + */ +@IonicPage({ segment: 'addon-mod-quiz-navigation-modal' }) +@Component({ + selector: 'page-addon-mod-quiz-navigation-modal', + templateUrl: 'navigation-modal.html', +}) +export class AddonModQuizNavigationModalPage { + + /** + * The instance of the page that opened the modal. We use the instance instead of the needed attributes for these reasons: + * - Some attributes can change dynamically, and we don't want to create the modal everytime the user opens it. + * - The onDidDismiss function takes a while to be called, making the app seem slow. This way we can directly call + * the functions we need without having to wait for the modal to be dismissed. + * @type {any} + */ + pageInstance: any; + + isReview: boolean; // Whether the user is reviewing the attempt. + + constructor(params: NavParams, protected viewCtrl: ViewController) { + this.isReview = !!params.get('isReview'); + this.pageInstance = params.get('page'); + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } + + /** + * Load a certain page. + * + * @param {number} page The page to load. + * @param {number} [slot] Slot of the question to scroll to. + */ + loadPage(page: number, slot: number): void { + this.pageInstance.changePage && this.pageInstance.changePage(page, true, slot); + this.closeModal(); + } + + /** + * Switch mode in review. + */ + switchMode(): void { + this.pageInstance.switchMode && this.pageInstance.switchMode(); + this.closeModal(); + } +} diff --git a/src/addon/mod/quiz/pages/player/player.html b/src/addon/mod/quiz/pages/player/player.html new file mode 100644 index 000000000..f7d0d0b53 --- /dev/null +++ b/src/addon/mod/quiz/pages/player/player.html @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+ + + +

{{ 'core.question.questionno' | translate:{$a: question.number} }}

+

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

+ +

{{question.status}}

+

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

{{ 'addon.mod_quiz.summaryofattempt' | translate }}

+
+ + + + {{ 'addon.mod_quiz.question' | translate }} + {{ 'addon.mod_quiz.status' | translate }} + + + + + + + {{ question.number }} + {{ question.status }} + + + + + + {{ 'addon.mod_quiz.returnattempt' | translate }} + + + + {{ attempt.dueDateWarning }} + + + + + +

{{ 'addon.mod_quiz.cannotsubmitquizdueto' | translate }}

+

{{message}}

+ + + {{ 'core.openinbrowser' | translate }} + +
+ + + {{ 'addon.mod_quiz.submitallandfinish' | translate }} + +
+ + + + +

{{ 'addon.mod_quiz.errorparsequestions' | translate }}

+
+ + + + {{ 'core.openinbrowser' | translate }} + + +
+
+
diff --git a/src/addon/mod/quiz/pages/player/player.module.ts b/src/addon/mod/quiz/pages/player/player.module.ts new file mode 100644 index 000000000..7275025c4 --- /dev/null +++ b/src/addon/mod/quiz/pages/player/player.module.ts @@ -0,0 +1,35 @@ +// (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 { CoreQuestionComponentsModule } from '@core/question/components/components.module'; +import { AddonModQuizPlayerPage } from './player'; + +@NgModule({ + declarations: [ + AddonModQuizPlayerPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CoreQuestionComponentsModule, + IonicPageModule.forChild(AddonModQuizPlayerPage), + TranslateModule.forChild() + ], +}) +export class AddonModQuizPlayerPageModule {} diff --git a/src/addon/mod/quiz/pages/player/player.scss b/src/addon/mod/quiz/pages/player/player.scss new file mode 100644 index 000000000..72ddb3b75 --- /dev/null +++ b/src/addon/mod/quiz/pages/player/player.scss @@ -0,0 +1,10 @@ +page-addon-mod-quiz-player { + .toolbar { + padding-top: 0; + padding-bottom: 0; + } + + .core-has-fixed-timer form { + padding-top: 56px; + } +} \ No newline at end of file diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts new file mode 100644 index 000000000..fddcbfa3f --- /dev/null +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -0,0 +1,580 @@ +// (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, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; +import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { AddonModQuizProvider } from '../../providers/quiz'; +import { AddonModQuizSyncProvider } from '../../providers/quiz-sync'; +import { AddonModQuizHelperProvider } from '../../providers/helper'; +import { AddonModQuizAutoSave } from '../../classes/auto-save'; +import { Subscription } from 'rxjs'; + +/** + * Page that allows attempting a quiz. + */ +@IonicPage({ segment: 'addon-mod-quiz-player' }) +@Component({ + selector: 'page-addon-mod-quiz-player', + templateUrl: 'player.html', +}) +export class AddonModQuizPlayerPage implements OnInit, OnDestroy { + @ViewChild(Content) content: Content; + + quiz: any; // The quiz the attempt belongs to. + attempt: any; // The attempt being attempted. + moduleUrl: string; // URL to the module in the site. + component = AddonModQuizProvider.COMPONENT; // Component to link the files to. + loaded: boolean; // Whether data has been loaded. + quizAborted: boolean; // Whether the quiz was aborted due to an error. + offline: boolean; // Whether the quiz is being attempted in offline mode. + navigation: any[]; // List of questions to navigate them. + questions: any[]; // Questions of the current page. + nextPage: number; // Next page. + previousPage: number; // Previous page. + showSummary: boolean; // Whether the attempt summary should be displayed. + summaryQuestions: any[]; // The questions to display in the summary. + canReturn: boolean; // Whether the user can return to a page after seeing the summary. + preventSubmitMessages: string[]; // List of messages explaining why the quiz cannot be submitted. + endTime: number; // The time when the attempt must be finished. + autoSaveError: boolean; // Whether there's been an error in auto-save. + navigationModal: Modal; // Modal to navigate through the questions. + + protected courseId: number; // The course ID the quiz belongs to. + protected quizId: number; // Quiz ID to attempt. + protected preflightData: any = {}; // Preflight data to attempt the quiz. + protected quizAccessInfo: any; // Quiz access information. + protected attemptAccessInfo: any; // Attempt access info. + protected lastAttempt: any; // Last user attempt before a new one is created (if needed). + protected newAttempt: boolean; // Whether the user is starting a new attempt. + protected quizDataLoaded: boolean; // Whether the quiz data has been loaded. + protected timeUpCalled: boolean; // Whether the time up function has been called. + protected autoSave: AddonModQuizAutoSave; // Class to auto-save answers every certain time. + protected autoSaveErrorSubscription: Subscription; // To be notified when an error happens in auto-save. + protected forceLeave = false; // If true, don't perform any check when leaving the view. + + constructor(navParams: NavParams, logger: CoreLoggerProvider, protected translate: TranslateService, + protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider, + protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController, + protected timeUtils: CoreTimeUtilsProvider, protected quizProvider: AddonModQuizProvider, + protected quizHelper: AddonModQuizHelperProvider, protected quizSync: AddonModQuizSyncProvider, + protected questionHelper: CoreQuestionHelperProvider, protected cdr: ChangeDetectorRef, + modalCtrl: ModalController, protected navCtrl: NavController) { + + this.quizId = navParams.get('quizId'); + this.courseId = navParams.get('courseId'); + this.moduleUrl = navParams.get('moduleUrl'); + + // Block the quiz so it cannot be synced. + this.syncProvider.blockOperation(AddonModQuizProvider.COMPONENT, this.quizId); + + // Create the auto save instance. + this.autoSave = new AddonModQuizAutoSave('addon-mod_quiz-player-form', '#addon-mod_quiz-connection-error-button', + logger, popoverCtrl, questionHelper, quizProvider); + + // Create the navigation modal. + this.navigationModal = modalCtrl.create('AddonModQuizNavigationModalPage', { + page: this + }); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Start the player when the page is loaded. + this.start(); + + // Listen for errors on auto-save. + this.autoSaveErrorSubscription = this.autoSave.onError().subscribe((error) => { + this.autoSaveError = error; + this.cdr.detectChanges(); + }); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + // Stop auto save. + this.autoSave.cancelAutoSave(); + this.autoSave.stopCheckChangesProcess(); + this.autoSaveErrorSubscription && this.autoSaveErrorSubscription.unsubscribe(); + + // Unblock the quiz so it can be synced. + this.syncProvider.unblockOperation(AddonModQuizProvider.COMPONENT, this.quizId); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (this.forceLeave) { + return true; + } + + if (this.questions && this.questions.length && !this.showSummary) { + // Save answers. + const modal = this.domUtils.showModalLoading('core.sending', true); + + return this.processAttempt(false, false).catch(() => { + // Save attempt failed. Show confirmation. + modal.dismiss(); + + return this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmleavequizonerror')); + }).finally(() => { + modal.dismiss(); + }); + } + + return Promise.resolve(); + } + + /** + * Abort the quiz. + */ + abortQuiz(): void { + this.quizAborted = true; + } + + /** + * A behaviour button in a question was clicked (Check, Redo, ...). + * + * @param {any} button Clicked button. + */ + behaviourButtonClicked(button: any): void { + // Confirm that the user really wants to do it. + this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => { + const modal = this.domUtils.showModalLoading('core.sending', true), + answers = this.getAnswers(); + + // Add the clicked button data. + answers[button.name] = button.value; + + // Behaviour checks are always in online. + return this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData).then(() => { + // Reload the current page. + const scrollElement = this.content.getScrollElement(), + scrollTop = scrollElement.scrollTop || 0, + scrollLeft = scrollElement.scrollLeft || 0; + + this.loaded = false; + this.content.scrollToTop(); // Scroll top so the spinner is seen. + + return this.loadPage(this.attempt.currentpage).finally(() => { + this.loaded = true; + this.content.scrollTo(scrollLeft, scrollTop); + }); + }).finally(() => { + modal.dismiss(); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error performing action.'); + }); + } + + /** + * Change the current page. If slot is supplied, try to scroll to that question. + * + * @param {number} page Page to load. -1 means summary. + * @param {boolean} [fromModal] Whether the page was selected using the navigation modal. + * @param {number} [slot] Slot of the question to scroll to. + */ + changePage(page: number, fromModal?: boolean, slot?: number): void { + if (page != -1 && (this.attempt.state == AddonModQuizProvider.ATTEMPT_OVERDUE || this.attempt.finishedOffline)) { + // We can't load a page if overdue or the local attempt is finished. + return; + } else if (page == this.attempt.currentpage && !this.showSummary && typeof slot != 'undefined') { + // Navigating to a question in the current page. + this.scrollToQuestion(slot); + + return; + } else if ((page == this.attempt.currentpage && !this.showSummary) || (fromModal && this.quiz.isSequential && page != -1)) { + // If the user is navigating to the current page we do nothing. + // Also, in sequential quizzes we don't allow navigating using the modal except for finishing the quiz (summary). + return; + } else if (page === -1 && this.showSummary) { + // Summary already shown. + return; + } + + this.loaded = false; + this.content.scrollToTop(); + + // First try to save the attempt data. We only save it if we're not seeing the summary. + const promise = this.showSummary ? Promise.resolve() : this.processAttempt(false, false); + promise.then(() => { + // Attempt data successfully saved, load the page or summary. + + // Attempt data successfully saved, load the page or summary. + let subPromise; + + // Stop checking for changes during page change. + this.autoSave.stopCheckChangesProcess(); + + if (page === -1) { + subPromise = this.loadSummary(); + } else { + subPromise = this.loadPage(page); + } + + return subPromise.catch((error) => { + // If the user isn't seeing the summary, start the check again. + if (!this.showSummary) { + this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline); + } + + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); + }); + }, (error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorsaveattempt', true); + }).finally(() => { + this.loaded = true; + + if (typeof slot != 'undefined') { + // Scroll to the question. Give some time to the questions to render. + setTimeout(() => { + this.scrollToQuestion(slot); + }, 2000); + } + }); + } + + /** + * Convenience function to get the quiz data. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchData(): Promise { + // Wait for any ongoing sync to finish. We won't sync a quiz while it's being played. + return this.quizSync.waitForSync(this.quizId).then(() => { + // Sync finished, now get the quiz. + return this.quizProvider.getQuizById(this.courseId, this.quizId); + }).then((quizData) => { + this.quiz = quizData; + this.quiz.isSequential = this.quizProvider.isNavigationSequential(this.quiz); + + if (this.quizProvider.isQuizOffline(this.quiz)) { + // Quiz supports offline. + return true; + } else { + // Quiz doesn't support offline right now, but maybe it did and then the setting was changed. + // If we have an unfinished offline attempt then we'll use offline mode. + return this.quizProvider.isLastAttemptOfflineUnfinished(this.quiz); + } + }).then((offlineMode) => { + this.offline = offlineMode; + + if (this.quiz.timelimit > 0) { + this.quiz.readableTimeLimit = this.timeUtils.formatTime(this.quiz.timelimit); + } + + // Get access information for the quiz. + return this.quizProvider.getQuizAccessInformation(this.quiz.id, this.offline, true); + }).then((info) => { + this.quizAccessInfo = info; + + // Get user attempts to determine last attempt. + return this.quizProvider.getUserAttempts(this.quiz.id, 'all', true, this.offline, true); + }).then((attempts) => { + if (!attempts.length) { + // There are no attempts, start a new one. + this.newAttempt = true; + } else { + const promises = []; + + // Get the last attempt. If it's finished, start a new one. + this.lastAttempt = attempts[attempts.length - 1]; + this.newAttempt = this.quizProvider.isAttemptFinished(this.lastAttempt.state); + + // Load quiz last sync time. + promises.push(this.quizSync.getSyncTime(this.quiz.id).then((time) => { + this.quiz.syncTime = time; + this.quiz.syncTimeReadable = this.quizSync.getReadableTimeFromTimestamp(time); + })); + + // Load flag to show if attempts are finished but not synced. + promises.push(this.quizProvider.loadFinishedOfflineData(attempts)); + + return Promise.all(promises); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true); + }); + } + + /** + * Finish an attempt, either by timeup or because the user clicked to finish it. + * + * @param {boolean} [userFinish] Whether the user clicked to finish the attempt. + * @param {boolean} [timeUp] Whether the quiz time is up. + * @return {Promise} Promise resolved when done. + */ + finishAttempt(userFinish?: boolean, timeUp?: boolean): Promise { + let promise; + + // Show confirm if the user clicked the finish button and the quiz is in progress. + if (!timeUp && this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { + promise = this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmclose')); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + const modal = this.domUtils.showModalLoading('core.sending', true); + + return this.processAttempt(userFinish, timeUp).then(() => { + // Trigger an event to notify the attempt was finished. + this.eventsProvider.trigger(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, { + quizId: this.quizId, + attemptId: this.attempt.id, + synced: !this.offline + }, this.sitesProvider.getCurrentSiteId()); + + // Leave the player. + this.forceLeave = true; + this.navCtrl.pop(); + }).finally(() => { + modal.dismiss(); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorsaveattempt', true); + }); + } + + /** + * Get the input answers. + * + * @return {any} Object with the answers. + */ + protected getAnswers(): any { + return this.questionHelper.getAnswersFromForm(document.forms['addon-mod_quiz-player-form']); + } + + /** + * Initializes the timer if enabled. + */ + protected initTimer(): void { + if (this.attemptAccessInfo.endtime > 0) { + // Quiz has an end time. Check if time left should be shown. + if (this.quizProvider.shouldShowTimeLeft(this.quizAccessInfo.activerulenames, this.attempt, + this.attemptAccessInfo.endtime)) { + this.endTime = this.attemptAccessInfo.endtime; + } else { + delete this.endTime; + } + } + } + + /** + * Load a page questions. + * + * @param {number} page The page to load. + * @return {Promise} Promise resolved when done. + */ + protected loadPage(page: number): Promise { + return this.quizProvider.getAttemptData(this.attempt.id, page, this.preflightData, this.offline, true).then((data) => { + // Update attempt, status could change during the execution. + this.attempt = data.attempt; + this.attempt.currentpage = page; + + this.questions = data.questions; + this.nextPage = data.nextpage; + this.previousPage = this.quiz.isSequential ? -1 : page - 1; + this.showSummary = false; + + this.questions.forEach((question) => { + // Get the readable mark for each question. + question.readableMark = this.quizHelper.getQuestionMarkFromHtml(question.html); + + // Extract the question info box. + this.questionHelper.extractQuestionInfoBox(question, '.info'); + + // Set the preferred behaviour. + question.preferredBehaviour = this.quiz.preferredbehaviour; + + // Check if the question is blocked. If it is, treat it as a description question. + if (this.quizProvider.isQuestionBlocked(question)) { + question.type = 'description'; + } + }); + + // Mark the page as viewed. We'll ignore errors in this call. + this.quizProvider.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline).catch((error) => { + // Ignore errors. + }); + + // Start looking for changes. + this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline); + }); + } + + /** + * Load attempt summary. + * + * @return {Promise} Promise resolved when done. + */ + protected loadSummary(): Promise { + this.summaryQuestions = []; + + return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline, true, true).then((qs) => { + this.showSummary = true; + this.summaryQuestions = qs; + + this.canReturn = this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS && !this.attempt.finishedOffline; + this.preventSubmitMessages = this.quizProvider.getPreventSubmitMessages(this.summaryQuestions); + + this.attempt.dueDateWarning = this.quizProvider.getAttemptDueDateWarning(this.quiz, this.attempt); + + // Log summary as viewed. + this.quizProvider.logViewAttemptSummary(this.attempt.id, this.preflightData).catch((error) => { + // Ignore errors. + }); + }); + } + + /** + * Load data to navigate the questions using the navigation modal. + * + * @return {Promise} Promise resolved when done. + */ + protected loadNavigation(): Promise { + // We use the attempt summary to build the navigation because it contains all the questions. + return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline).then((questions) => { + this.navigation = questions; + }); + } + + // Prepare the answers to be sent for the attempt. + protected prepareAnswers(): Promise { + return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline); + } + + /** + * Process attempt. + * + * @param {boolean} [userFinish] Whether the user clicked to finish the attempt. + * @param {boolean} [timeUp] Whether the quiz time is up. + * @return {Promise} Promise resolved when done. + */ + protected processAttempt(userFinish?: boolean, timeUp?: boolean): Promise { + // Get the answers to send. + return this.prepareAnswers().then((answers) => { + // Send the answers. + return this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData, userFinish, timeUp, + this.offline); + }).then(() => { + // Answers saved, cancel auto save. + this.autoSave.cancelAutoSave(); + this.autoSave.hideAutoSaveError(); + }); + } + + /** + * Scroll to a certain question. + * + * @param {number} slot Slot of the question to scroll to. + */ + protected scrollToQuestion(slot: number): void { + this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_quiz-question-' + slot); + } + + /** + * Show connection error. + * + * @param {Event} ev Click event. + */ + showConnectionError(ev: Event): void { + this.autoSave.showAutoSaveError(ev); + } + + /** + * Convenience function to start the player. + */ + start(): void { + let promise; + this.loaded = false; + + if (this.quizDataLoaded) { + // Quiz data has been loaded, try to start or continue. + promise = this.startOrContinueAttempt(); + } else { + // Fetch data. + promise = this.fetchData().then(() => { + this.quizDataLoaded = true; + + return this.startOrContinueAttempt(); + }); + } + + promise.finally(() => { + this.loaded = true; + }); + } + + /** + * Start or continue an attempt. + * + * @return {Promise} [description] + */ + protected startOrContinueAttempt(): Promise { + const attempt = this.newAttempt ? undefined : this.lastAttempt; + + // Get the preflight data and start attempt if needed. + return this.quizHelper.getAndCheckPreflightData(this.quiz, this.quizAccessInfo, this.preflightData, attempt, this.offline, + false, 'addon.mod_quiz.startattempt').then((attempt) => { + + // Re-fetch attempt access information with the right attempt (might have changed because a new attempt was created). + return this.quizProvider.getAttemptAccessInformation(this.quiz.id, attempt.id, this.offline, true).then((info) => { + this.attemptAccessInfo = info; + this.attempt = attempt; + + return this.loadNavigation(); + }).then(() => { + if (this.attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !this.attempt.finishedOffline) { + // Attempt not overdue and not finished in offline, load page. + return this.loadPage(this.attempt.currentpage).then(() => { + this.initTimer(); + }); + } else { + // Attempt is overdue or finished in offline, we can only load the summary. + return this.loadSummary(); + } + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); + }); + } + + /** + * Quiz time has finished. + */ + timeUp(): void { + if (this.timeUpCalled) { + return; + } + + this.timeUpCalled = true; + this.finishAttempt(false, true); + } +} diff --git a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html new file mode 100644 index 000000000..db49a795e --- /dev/null +++ b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html @@ -0,0 +1,27 @@ + + + {{ title | translate }} + + + + + + + +
+ + + +

Couldn't find the directive to render this access rule.

+
+ +
+ + +
+
+
diff --git a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.module.ts b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.module.ts new file mode 100644 index 000000000..740c8f325 --- /dev/null +++ b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.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 { AddonModQuizPreflightModalPage } from './preflight-modal'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; + +@NgModule({ + declarations: [ + AddonModQuizPreflightModalPage + ], + imports: [ + CoreComponentsModule, + IonicPageModule.forChild(AddonModQuizPreflightModalPage), + TranslateModule.forChild() + ] +}) +export class AddonModQuizPreflightModalModule {} diff --git a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts new file mode 100644 index 000000000..48e704f54 --- /dev/null +++ b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts @@ -0,0 +1,122 @@ +// (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, OnInit, Injector, ViewChild } from '@angular/core'; +import { IonicPage, ViewController, NavParams, Content } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; + +/** + * Modal that renders the access rules for a quiz. + */ +@IonicPage({ segment: 'addon-mod-quiz-preflight-modal' }) +@Component({ + selector: 'page-addon-mod-quiz-preflight-modal', + templateUrl: 'preflight-modal.html', +}) +export class AddonModQuizPreflightModalPage implements OnInit { + + @ViewChild(Content) content: Content; + + preflightForm: FormGroup; + title: string; + accessRulesComponent: any[] = []; + data: any; + loaded: boolean; + + protected quiz: any; + protected attempt: any; + protected prefetch: boolean; + protected siteId: string; + protected rules: string[]; + protected renderedRules: string[] = []; + + constructor(params: NavParams, fb: FormBuilder, translate: TranslateService, sitesProvider: CoreSitesProvider, + protected viewCtrl: ViewController, protected accessRuleDelegate: AddonModQuizAccessRuleDelegate, + protected injector: Injector, protected domUtils: CoreDomUtilsProvider) { + + this.title = params.get('title') || translate.instant('addon.mod_quiz.startattempt'); + this.quiz = params.get('quiz'); + this.attempt = params.get('attempt'); + this.prefetch = params.get('prefetch'); + this.siteId = params.get('siteId') || sitesProvider.getCurrentSiteId(); + this.rules = params.get('rules') || []; + + // Create an empty form group. The controls will be added by the access rules components. + this.preflightForm = fb.group({}); + + // Create the data to pass to the access rules components. + this.data = { + quiz: this.quiz, + attempt: this.attempt, + prefetch: this.prefetch, + form: this.preflightForm, + siteId: this.siteId + }; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + const promises = []; + + this.rules.forEach((rule) => { + // Check if preflight is required for rule and, if so, get the component to render it. + promises.push(this.accessRuleDelegate.isPreflightCheckRequiredForRule(rule, this.quiz, this.attempt, this.prefetch, + this.siteId).then((required) => { + + if (required) { + return this.accessRuleDelegate.getPreflightComponent(rule, this.injector).then((component) => { + if (component) { + this.renderedRules.push(rule); + this.accessRulesComponent.push(component); + } + }); + } + })); + }); + + Promise.all(promises).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error loading rules'); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Check that the data is valid and send it back. + */ + sendData(): void { + if (!this.preflightForm.valid) { + // Form not valid. Scroll to the first element with errors. + if (!this.domUtils.scrollToInputError(this.content)) { + // Input not found, show an error modal. + this.domUtils.showErrorModal('core.errorinvalidform', true); + } + } else { + this.viewCtrl.dismiss(this.preflightForm.value); + } + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } +} diff --git a/src/addon/mod/quiz/pages/review/review.html b/src/addon/mod/quiz/pages/review/review.html new file mode 100644 index 000000000..6714b5e88 --- /dev/null +++ b/src/addon/mod/quiz/pages/review/review.html @@ -0,0 +1,102 @@ + + + {{ 'addon.mod_quiz.review' | translate }} + + + + + + + + + + + + + + + +

{{ 'addon.mod_quiz.reviewofpreview' | translate }}

+

{{ 'addon.mod_quiz.reviewofattempt' | translate:{$a: attempt.attempt} }}

+
+ + +

{{ 'addon.mod_quiz.startedon' | translate }}

+

{{ attempt.timestart * 1000 | coreFormatDate:"dfmediumdate" }}

+
+ +

{{ 'addon.mod_quiz.attemptstate' | translate }}

+

{{ attempt.readableState }}

+
+ +

{{ 'addon.mod_quiz.completedon' | translate }}

+

{{ attempt.timefinish * 1000 | coreFormatDate:"dfmediumdate" }}

+
+ +

{{ 'addon.mod_quiz.timetaken' | translate }}

+

{{ attempt.timeTaken }}

+
+ +

{{ 'addon.mod_quiz.overdue' | translate }}

+

{{ attempt.overTime }}

+
+ +

{{ 'addon.mod_quiz.marks' | translate }}

+

+
+ +

{{ 'addon.mod_quiz.grade' | translate }}

+

{{ attempt.readableGrade }}

+
+ +

{{ data.title }}

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

{{ 'core.question.questionno' | translate:{$a: question.number} }}

+

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

+ +

{{question.status}}

+

+
+
+ + +
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + diff --git a/src/addon/mod/quiz/pages/review/review.module.ts b/src/addon/mod/quiz/pages/review/review.module.ts new file mode 100644 index 000000000..a0d03f60f --- /dev/null +++ b/src/addon/mod/quiz/pages/review/review.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 { CoreQuestionComponentsModule } from '@core/question/components/components.module'; +import { AddonModQuizReviewPage } from './review'; + +@NgModule({ + declarations: [ + AddonModQuizReviewPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + CoreQuestionComponentsModule, + IonicPageModule.forChild(AddonModQuizReviewPage), + TranslateModule.forChild() + ], +}) +export class AddonModQuizReviewPageModule {} diff --git a/src/addon/mod/quiz/pages/review/review.scss b/src/addon/mod/quiz/pages/review/review.scss new file mode 100644 index 000000000..26a67cda3 --- /dev/null +++ b/src/addon/mod/quiz/pages/review/review.scss @@ -0,0 +1,11 @@ +page-addon-mod-quiz-review { + .item-radio-disabled, + .item-checkbox-disabled, + .text-input[disabled] { + opacity: 1; + + .label, .radio, .checkbox { + opacity: 1; + } + } +} diff --git a/src/addon/mod/quiz/pages/review/review.ts b/src/addon/mod/quiz/pages/review/review.ts new file mode 100644 index 000000000..05d9c9c16 --- /dev/null +++ b/src/addon/mod/quiz/pages/review/review.ts @@ -0,0 +1,297 @@ +// (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, OnInit, ViewChild } from '@angular/core'; +import { IonicPage, NavParams, Content, ModalController, Modal } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { AddonModQuizProvider } from '../../providers/quiz'; +import { AddonModQuizHelperProvider } from '../../providers/helper'; + +/** + * Page that allows reviewing a quiz attempt. + */ +@IonicPage({ segment: 'addon-mod-quiz-review' }) +@Component({ + selector: 'page-addon-mod-quiz-review', + templateUrl: 'review.html', +}) +export class AddonModQuizReviewPage implements OnInit { + @ViewChild(Content) content: Content; + + attempt: any; // The attempt being reviewed. + component = AddonModQuizProvider.COMPONENT; // Component to link the files to. + componentId: number; // ID to use in conjunction with the component. + showAll: boolean; // Whether to view all questions in the same page. + numPages: number; // Number of pages. + showCompleted: boolean; // Whether to show completed time. + additionalData: any[]; // Additional data to display for the attempt. + loaded: boolean; // Whether data has been loaded. + navigation: any[]; // List of questions to navigate them. + questions: any[]; // Questions of the current page. + nextPage: number; // Next page. + previousPage: number; // Previous page. + navigationModal: Modal; // Modal to navigate through the questions. + + protected quiz: any; // The quiz the attempt belongs to. + protected courseId: number; // The course ID the quiz belongs to. + protected quizId: number; // Quiz ID the attempt belongs to. + protected attemptId: number; // The attempt being reviewed. + protected currentPage: number; // The current page being reviewed. + protected options: any; // Review options. + + constructor(navParams: NavParams, modalCtrl: ModalController, protected translate: TranslateService, + protected domUtils: CoreDomUtilsProvider, protected timeUtils: CoreTimeUtilsProvider, + protected quizProvider: AddonModQuizProvider, protected quizHelper: AddonModQuizHelperProvider, + protected questionHelper: CoreQuestionHelperProvider, protected textUtils: CoreTextUtilsProvider) { + + this.quizId = navParams.get('quizId'); + this.courseId = navParams.get('courseId'); + this.attemptId = navParams.get('attemptId'); + this.currentPage = navParams.get('page') || -1; + this.showAll = this.currentPage == -1; + + // Create the navigation modal. + this.navigationModal = modalCtrl.create('AddonModQuizNavigationModalPage', { + isReview: true, + page: this + }); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchData().then(() => { + this.quizProvider.logViewAttemptReview(this.attemptId).catch((error) => { + // Ignore errors. + }); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Change the current page. If slot is supplied, try to scroll to that question. + * + * @param {number} page Page to load. -1 means all questions in same page. + * @param {boolean} [fromModal] Whether the page was selected using the navigation modal. + * @param {number} [slot] Slot of the question to scroll to. + */ + changePage(page: number, fromModal?: boolean, slot?: number): void { + if (typeof slot != 'undefined' && (this.attempt.currentpage == -1 || page == this.currentPage)) { + // Scrol to a certain question in the current page. + this.scrollToQuestion(slot); + + return; + } else if (page == this.currentPage) { + // If the user is navigating to the current page and no question specified, we do nothing. + return; + } + + this.loaded = false; + this.content.scrollToTop(); + + this.loadPage(page).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); + }).finally(() => { + this.loaded = true; + + if (typeof slot != 'undefined') { + // Scroll to the question. Give some time to the questions to render. + setTimeout(() => { + this.scrollToQuestion(slot); + }, 2000); + } + }); + } + + /** + * Convenience function to get the quiz data. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchData(): Promise { + return this.quizProvider.getQuizById(this.courseId, this.quizId).then((quizData) => { + this.quiz = quizData; + this.componentId = this.quiz.coursemodule; + + return this.quizProvider.getCombinedReviewOptions(this.quizId).then((result) => { + this.options = result; + + // Load the navigation data. + return this.loadNavigation().then(() => { + // Load questions. + return this.loadPage(this.currentPage); + }); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true); + }); + } + + /** + * Load a page questions. + * + * @param {number} page The page to load. + * @return {Promise} Promise resolved when done. + */ + protected loadPage(page: number): Promise { + return this.quizProvider.getAttemptReview(this.attemptId, page).then((data) => { + this.attempt = data.attempt; + this.attempt.currentpage = page; + this.currentPage = page; + + // Set the summary data. + this.setSummaryCalculatedData(data); + + this.questions = data.questions; + this.nextPage = page == -1 ? undefined : page + 1; + this.previousPage = page - 1; + + this.questions.forEach((question) => { + // Get the readable mark for each question. + question.readableMark = this.quizHelper.getQuestionMarkFromHtml(question.html); + + // Extract the question info box. + this.questionHelper.extractQuestionInfoBox(question, '.info'); + + // Set the preferred behaviour. + question.preferredBehaviour = this.quiz.preferredbehaviour; + }); + }); + } + + /** + * Load data to navigate the questions using the navigation modal. + * + * @return {Promise} Promise resolved when done. + */ + protected loadNavigation(): Promise { + // Get all questions in single page to retrieve all the questions. + return this.quizProvider.getAttemptReview(this.attemptId, -1).then((data) => { + const lastQuestion = data.questions[data.questions.length - 1]; + + data.questions.forEach((question) => { + question.stateClass = this.questionHelper.getQuestionStateClass(question.state); + }); + + this.navigation = data.questions; + this.numPages = lastQuestion ? lastQuestion.page + 1 : 0; + }); + } + + /** + * Refreshes data. + * + * @param {any} refresher Refresher + */ + refreshData(refresher: any): void { + const promises = []; + + promises.push(this.quizProvider.invalidateQuizData(this.courseId)); + promises.push(this.quizProvider.invalidateCombinedReviewOptionsForUser(this.quizId)); + promises.push(this.quizProvider.invalidateAttemptReview(this.attemptId)); + + Promise.all(promises).finally(() => { + return this.fetchData(); + }).finally(() => { + refresher.complete(); + }); + } + + /** + * Scroll to a certain question. + * + * @param {number} slot Slot of the question to scroll to. + */ + protected scrollToQuestion(slot: number): void { + this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_quiz-question-' + slot); + } + + /** + * Calculate review summary data. + * + * @param {any} data Result of getAttemptReview. + */ + protected setSummaryCalculatedData(data: any): void { + + this.attempt.readableState = this.quizProvider.getAttemptReadableStateName(this.attempt.state); + + if (this.attempt.state == AddonModQuizProvider.ATTEMPT_FINISHED) { + this.showCompleted = true; + this.additionalData = data.additionaldata; + + const timeTaken = this.attempt.timefinish - this.attempt.timestart; + if (timeTaken) { + // Format time taken. + this.attempt.timeTaken = this.timeUtils.formatTime(timeTaken); + + // Calculate overdue time. + if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) { + this.attempt.overTime = this.timeUtils.formatTime(timeTaken - this.quiz.timelimit); + } + } + + // Treat grade. + if (this.options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX && + this.quizProvider.quizHasGrades(this.quiz)) { + + if (data.grade === null || typeof data.grade == 'undefined') { + this.attempt.readableGrade = this.quizProvider.formatGrade(data.grade, this.quiz.decimalpoints); + } else { + // Show raw marks only if they are different from the grade (like on the entry page). + if (this.quiz.grade != this.quiz.sumgrades) { + this.attempt.readableMark = this.translate.instant('addon.mod_quiz.outofshort', {$a: { + grade: this.quizProvider.formatGrade(this.attempt.sumgrades, this.quiz.decimalpoints), + maxgrade: this.quizProvider.formatGrade(this.quiz.sumgrades, this.quiz.decimalpoints) + }}); + } + + // Now the scaled grade. + const gradeObject: any = { + grade: this.quizProvider.formatGrade(data.grade, this.quiz.decimalpoints), + maxgrade: this.quizProvider.formatGrade(this.quiz.grade, this.quiz.decimalpoints) + }; + + if (this.quiz.grade != 100) { + gradeObject.percent = this.textUtils.roundToDecimals(this.attempt.sumgrades * 100 / this.quiz.sumgrades, 0); + this.attempt.readableGrade = this.translate.instant('addon.mod_quiz.outofpercent', {$a: gradeObject}); + } else { + this.attempt.readableGrade = this.translate.instant('addon.mod_quiz.outof', {$a: gradeObject}); + } + } + } + + // Treat additional data. + this.additionalData.forEach((data) => { + // Remove help links from additional data. + data.content = this.domUtils.removeElementFromHtml(data.content, '.helptooltip'); + }); + } + } + + /** + * Switch mode: all questions in same page OR one page at a time. + */ + switchMode(): void { + this.showAll = !this.showAll; + + // Load all questions or first page, depending on the mode. + this.loadPage(this.showAll ? -1 : 0); + } +} diff --git a/src/addon/mod/quiz/providers/access-rules-delegate.ts b/src/addon/mod/quiz/providers/access-rules-delegate.ts new file mode 100644 index 000000000..ace7c6c69 --- /dev/null +++ b/src/addon/mod/quiz/providers/access-rules-delegate.ts @@ -0,0 +1,295 @@ +// (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 { CoreLoggerProvider } from '@providers/logger'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; + +/** + * Interface that all access rules handlers must implement. + */ +export interface AddonModQuizAccessRuleHandler extends CoreDelegateHandler { + + /** + * Name of the rule the handler supports. E.g. 'password'. + * @type {string} + */ + ruleName: string; + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise; + + /** + * Add preflight data that doesn't require user interaction. The data should be added to the preflightData param. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} preflightData Object where to add the preflight data. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + getFixedPreflightData?(quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise; + + /** + * Return the Component to use to display the access rule preflight. + * Implement this if your access rule requires a preflight check with user interaction. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getPreflightComponent?(injector: Injector): any | Promise; + + /** + * Function called when the preflight check has passed. This is a chance to record that fact in some way. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} attempt The attempt started/continued. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + notifyPreflightCheckPassed?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : void | Promise; + + /** + * Function called when the preflight check fails. This is a chance to record that fact in some way. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} attempt The attempt started/continued. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + notifyPreflightCheckFailed?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : void | Promise; + + /** + * Whether or not the time left of an attempt should be displayed. + * + * @param {any} attempt The attempt. + * @param {number} endTime The attempt end time (in seconds). + * @param {number} timeNow The current time in seconds. + * @return {boolean} Whether it should be displayed. + */ + shouldShowTimeLeft?(attempt: any, endTime: number, timeNow: number): boolean; +} + +/** + * Delegate to register access rules for quiz module. + */ +@Injectable() +export class AddonModQuizAccessRuleDelegate extends CoreDelegate { + + protected handlerNameProperty = 'ruleName'; + + constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + protected utils: CoreUtilsProvider) { + super('AddonModQuizAccessRulesDelegate', logger, sitesProvider, eventsProvider); + } + + /** + * Get the handler for a certain rule. + * + * @param {string} ruleName Name of the access rule. + * @return {AddonModQuizAccessRuleHandler} Handler. Undefined if no handler found for the rule. + */ + getAccessRuleHandler(ruleName: string): AddonModQuizAccessRuleHandler { + return this.getHandler(ruleName, true); + } + + /** + * Given a list of rules, get some fixed preflight data (data that doesn't require user interaction). + * + * @param {string[]} rules List of active rules names. + * @param {any} quiz Quiz. + * @param {any} preflightData Object where to store the preflight data. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when all the data has been gathered. + */ + getFixedPreflightData(rules: string[], quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string) + : Promise { + rules = rules || []; + + const promises = []; + rules.forEach((rule) => { + promises.push(Promise.resolve( + this.executeFunctionOnEnabled(rule, 'getFixedPreflightData', [quiz, preflightData, attempt, prefetch, siteId]) + )); + }); + + return this.utils.allPromises(promises).catch(() => { + // Never reject. + }); + } + + /** + * Get the Component to use to display the access rule preflight. + * + * @param {Injector} injector Injector. + * @return {Promise} Promise resolved with the component to use, undefined if not found. + */ + getPreflightComponent(rule: string, injector: Injector): Promise { + return Promise.resolve(this.executeFunctionOnEnabled(rule, 'getPreflightComponent', [injector])); + } + + /** + * Check if an access rule is supported. + * + * @param {string} ruleName Name of the rule. + * @return {boolean} Whether it's supported. + */ + isAccessRuleSupported(ruleName: string): boolean { + return this.hasHandler(ruleName, true); + } + + /** + * Given a list of rules, check if preflight check is required. + * + * @param {string[]} rules List of active rules names. + * @param {any} quiz Quiz. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether it's required. + */ + isPreflightCheckRequired(rules: string[], quiz: any, attempt: any, prefetch?: boolean, siteId?: string): Promise { + rules = rules || []; + + const promises = []; + let isRequired = false; + + rules.forEach((rule) => { + promises.push(this.isPreflightCheckRequiredForRule(rule, quiz, attempt, prefetch, siteId).then((required) => { + if (required) { + isRequired = true; + } + })); + }); + + return this.utils.allPromises(promises).then(() => { + return isRequired; + }).catch(() => { + // Never reject. + return isRequired; + }); + } + + /** + * Check if preflight check is required for a certain rule. + * + * @param {string} rule Rule name. + * @param {any} quiz Quiz. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether it's required. + */ + isPreflightCheckRequiredForRule(rule: string, quiz: any, attempt: any, prefetch?: boolean, siteId?: string): Promise { + return Promise.resolve(this.executeFunctionOnEnabled(rule, 'isPreflightCheckRequired', [quiz, attempt, prefetch, siteId])); + } + + /** + * Notify all rules that the preflight check has passed. + * + * @param {string[]} rules List of active rules names. + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + notifyPreflightCheckPassed(rules: string[], quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : Promise { + rules = rules || []; + + const promises = []; + rules.forEach((rule) => { + promises.push(Promise.resolve( + this.executeFunctionOnEnabled(rule, 'notifyPreflightCheckPassed', [quiz, attempt, preflightData, prefetch, siteId]) + )); + }); + + return this.utils.allPromises(promises).catch(() => { + // Never reject. + }); + } + + /** + * Notify all rules that the preflight check has failed. + * + * @param {string[]} rules List of active rules names. + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + notifyPreflightCheckFailed(rules: string[], quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : Promise { + rules = rules || []; + + const promises = []; + rules.forEach((rule) => { + promises.push(Promise.resolve( + this.executeFunctionOnEnabled(rule, 'notifyPreflightCheckFailed', [quiz, attempt, preflightData, prefetch, siteId]) + )); + }); + + return this.utils.allPromises(promises).catch(() => { + // Never reject. + }); + } + + /** + * Whether or not the time left of an attempt should be displayed. + * + * @param {string[]} rules List of active rules names. + * @param {any} attempt The attempt. + * @param {number} endTime The attempt end time (in seconds). + * @param {number} timeNow The current time in seconds. + * @return {boolean} Whether it should be displayed. + */ + shouldShowTimeLeft(rules: string[], attempt: any, endTime: number, timeNow: number): boolean { + rules = rules || []; + + for (const i in rules) { + const rule = rules[i]; + + if (this.executeFunctionOnEnabled(rule, 'shouldShowTimeLeft', [attempt, endTime, timeNow])) { + return true; + } + } + + return false; + } +} diff --git a/src/addon/mod/quiz/providers/grade-link-handler.ts b/src/addon/mod/quiz/providers/grade-link-handler.ts new file mode 100644 index 000000000..1cb006b8c --- /dev/null +++ b/src/addon/mod/quiz/providers/grade-link-handler.ts @@ -0,0 +1,48 @@ +// (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 { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreContentLinksModuleGradeHandler } from '@core/contentlinks/classes/module-grade-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModQuizProvider } from './quiz'; + +/** + * Handler to treat links to quiz grade. + */ +@Injectable() +export class AddonModQuizGradeLinkHandler extends CoreContentLinksModuleGradeHandler { + name = 'AddonModQuizGradeLinkHandler'; + canReview = false; + + constructor(courseHelper: CoreCourseHelperProvider, domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider, + protected quizProvider: AddonModQuizProvider) { + super(courseHelper, domUtils, sitesProvider, 'AddonModQuiz', 'quiz'); + } + + /** + * 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.quizProvider.isPluginEnabled(); + } +} diff --git a/src/addon/mod/quiz/providers/helper.ts b/src/addon/mod/quiz/providers/helper.ts new file mode 100644 index 000000000..3093b3994 --- /dev/null +++ b/src/addon/mod/quiz/providers/helper.ts @@ -0,0 +1,270 @@ +// (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 { ModalController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonModQuizProvider } from './quiz'; +import { AddonModQuizOfflineProvider } from './quiz-offline'; +import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate'; + +/** + * Helper service that provides some features for quiz. + */ +@Injectable() +export class AddonModQuizHelperProvider { + + protected div = document.createElement('div'); // A div element to search in HTML code. + + constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider, + private accessRuleDelegate: AddonModQuizAccessRuleDelegate, private quizProvider: AddonModQuizProvider, + private modalCtrl: ModalController, private quizOfflineProvider: AddonModQuizOfflineProvider) { } + + /** + * Validate a preflight data or show a modal to input the preflight data if required. + * It calls AddonModQuizProvider.startAttempt if a new attempt is needed. + * + * @param {any} quiz Quiz. + * @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation. + * @param {any} preflightData Object where to store the preflight data. + * @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt. + * @param {boolean} [offline] Whether the attempt is offline. + * @param {boolean} [prefetch] Whether user is prefetching. + * @param {string} [title] The title to display in the modal and in the submit button. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [retrying] Whether we're retrying after a failure. + * @return {Promise} Promise resolved when the preflight data is validated. The resolve param is the attempt. + */ + getAndCheckPreflightData(quiz: any, accessInfo: any, preflightData: any, attempt: any, offline?: boolean, prefetch?: boolean, + title?: string, siteId?: string, retrying?: boolean): Promise { + + const rules = accessInfo.activerulenames; + let isPreflightCheckRequired = false; + + // Check if the user needs to input preflight data. + return this.accessRuleDelegate.isPreflightCheckRequired(rules, quiz, attempt, prefetch, siteId).then((required) => { + isPreflightCheckRequired = required; + + if (required) { + // Preflight check is required but no preflightData has been sent. Show a modal with the preflight form. + return this.getPreflightData(quiz, accessInfo, attempt, prefetch, title, siteId).then((data) => { + // Data entered by the user, add it to preflight data and check it again. + Object.assign(preflightData, data); + }); + } + }).then(() => { + // Get some fixed preflight data from access rules (data that doesn't require user interaction). + return this.accessRuleDelegate.getFixedPreflightData(rules, quiz, preflightData, attempt, prefetch, siteId); + }).then(() => { + + // All the preflight data is gathered, now validate it. + return this.validatePreflightData(quiz, accessInfo, preflightData, attempt, offline, prefetch, siteId) + .catch((error) => { + + if (prefetch) { + return Promise.reject(error); + } else if (retrying && !isPreflightCheckRequired) { + // We're retrying after a failure, but the preflight check wasn't required. + // This means there's something wrong with some access rule or user is offline and data isn't cached. + // Don't retry again because it would lead to an infinite loop. + return Promise.reject(error); + } else { + // Show error and ask for the preflight again. + // Wait to show the error because we want it to be shown over the preflight modal. + setTimeout(() => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }, 100); + + return this.getAndCheckPreflightData(quiz, accessInfo, preflightData, attempt, offline, prefetch, + title, siteId, true); + } + }); + }); + } + + /** + * Get the preflight data from the user using a modal. + * + * @param {any} quiz Quiz. + * @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [title] The title to display in the modal and in the submit button. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the preflight data. Rejected if user cancels. + */ + getPreflightData(quiz: any, accessInfo: any, attempt: any, prefetch?: boolean, title?: string, siteId?: string): Promise { + const notSupported: string[] = []; + + // Check if there is any unsupported rule. + accessInfo.activerulenames.forEach((rule) => { + if (!this.accessRuleDelegate.isAccessRuleSupported(rule)) { + notSupported.push(rule); + } + }); + + if (notSupported.length) { + return Promise.reject(this.translate.instant('addon.mod_quiz.errorrulesnotsupported') + ' ' + + JSON.stringify(notSupported)); + } + + // Create and show the modal. + const modal = this.modalCtrl.create('AddonModQuizPreflightModalPage', { + title: title, + quiz: quiz, + attempt: attempt, + prefetch: !!prefetch, + siteId: siteId, + rules: accessInfo.activerulenames + }); + + modal.present(); + + // Wait for modal to be dismissed. + return new Promise((resolve, reject): void => { + modal.onDidDismiss((data) => { + if (typeof data != 'undefined') { + resolve(data); + } else { + reject(this.domUtils.createCanceledError()); + } + }); + }); + } + + /** + * Gets the mark string from a question HTML. + * Example result: "Marked out of 1.00". + * + * @param {string} html Question's HTML. + * @return {string} Question's mark. + */ + getQuestionMarkFromHtml(html: string): string { + this.div.innerHTML = html; + + return this.domUtils.getContentsOfElement(this.div, '.grade'); + } + + /** + * Add some calculated data to the attempt. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {boolean} highlight Whether we should check if attempt should be highlighted. + * @param {number} [bestGrade] Quiz's best grade (formatted). Required if highlight=true. + */ + setAttemptCalculatedData(quiz: any, attempt: any, highlight?: boolean, bestGrade?: string): void { + + attempt.rescaledGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false); + attempt.finished = this.quizProvider.isAttemptFinished(attempt.state); + attempt.readableState = this.quizProvider.getAttemptReadableState(quiz, attempt); + + if (quiz.showMarkColumn && attempt.finished) { + attempt.readableMark = this.quizProvider.formatGrade(attempt.sumgrades, quiz.decimalpoints); + } else { + attempt.readableMark = ''; + } + + if (quiz.showGradeColumn && attempt.finished) { + attempt.readableGrade = this.quizProvider.formatGrade(attempt.rescaledGrade, quiz.decimalpoints); + + // Highlight the highest grade if appropriate. + attempt.highlightGrade = highlight && !attempt.preview && attempt.state == AddonModQuizProvider.ATTEMPT_FINISHED && + attempt.readableGrade == bestGrade; + } else { + attempt.readableGrade = ''; + } + } + + /** + * Add some calculated data to the quiz. + * + * @param {any} quiz Quiz. + * @param {any} options Options returned by AddonModQuizProvider.getCombinedReviewOptions. + */ + setQuizCalculatedData(quiz: any, options: any): void { + quiz.sumGradesFormatted = this.quizProvider.formatGrade(quiz.sumgrades, quiz.decimalpoints); + quiz.gradeFormatted = this.quizProvider.formatGrade(quiz.grade, quiz.decimalpoints); + + quiz.showAttemptColumn = quiz.attempts != 1; + quiz.showGradeColumn = options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX && + this.quizProvider.quizHasGrades(quiz); + quiz.showMarkColumn = quiz.showGradeColumn && quiz.grade != quiz.sumgrades; + quiz.showFeedbackColumn = quiz.hasfeedback && options.alloptions.overallfeedback; + } + + /** + * Validate the preflight data. It calls AddonModQuizProvider.startAttempt if a new attempt is needed. + * + * @param {any} quiz Quiz. + * @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation. + * @param {any} preflightData Object where to store the preflight data. + * @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt. + * @param {boolean} [offline] Whether the attempt is offline. + * @param {boolean} [sent] Whether preflight data has been entered by the user. + * @param {boolean} [prefetch] Whether user is prefetching. + * @param {string} [title] The title to display in the modal and in the submit button. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the preflight data is validated. + */ + validatePreflightData(quiz: any, accessInfo: any, preflightData: any, attempt: any, offline?: boolean, prefetch?: boolean, + siteId?: string): Promise { + + const rules = accessInfo.activerulenames; + let promise; + + if (attempt) { + if (attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !attempt.finishedOffline) { + // We're continuing an attempt. Call getAttemptData to validate the preflight data. + const page = attempt.currentpage; + + promise = this.quizProvider.getAttemptData(attempt.id, page, preflightData, offline, true, siteId).then(() => { + if (offline) { + // Get current page stored in local. + return this.quizOfflineProvider.getAttemptById(attempt.id).then((localAttempt) => { + attempt.currentpage = localAttempt.currentpage; + }).catch(() => { + // No local data. + }); + } + }); + } else { + // Attempt is overdue or finished in offline, we can only see the summary. + // Call getAttemptSummary to validate the preflight data. + promise = this.quizProvider.getAttemptSummary(attempt.id, preflightData, offline, true, false, siteId); + } + } else { + // We're starting a new attempt, call startAttempt. + promise = this.quizProvider.startAttempt(quiz.id, preflightData, false, siteId).then((att) => { + attempt = att; + }); + } + + return promise.then(() => { + // Preflight data validated. + this.accessRuleDelegate.notifyPreflightCheckPassed(rules, quiz, attempt, preflightData, prefetch, siteId); + + return attempt; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService returned an error, assume the preflight failed. + this.accessRuleDelegate.notifyPreflightCheckFailed(rules, quiz, attempt, preflightData, prefetch, siteId); + } + + return Promise.reject(error); + }); + } +} diff --git a/src/addon/mod/quiz/providers/index-link-handler.ts b/src/addon/mod/quiz/providers/index-link-handler.ts new file mode 100644 index 000000000..e3181f2b8 --- /dev/null +++ b/src/addon/mod/quiz/providers/index-link-handler.ts @@ -0,0 +1,44 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModQuizProvider } from './quiz'; + +/** + * Handler to treat links to quiz index. + */ +@Injectable() +export class AddonModQuizIndexLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModQuizIndexLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider, protected quizProvider: AddonModQuizProvider) { + super(courseHelper, 'AddonModQuiz', 'quiz'); + } + + /** + * 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.quizProvider.isPluginEnabled(); + } +} diff --git a/src/addon/mod/quiz/providers/module-handler.ts b/src/addon/mod/quiz/providers/module-handler.ts new file mode 100644 index 000000000..de7e2af28 --- /dev/null +++ b/src/addon/mod/quiz/providers/module-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 { NavController, NavOptions } from 'ionic-angular'; +import { AddonModQuizIndexComponent } from '../components/index/index'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; + +/** + * Handler to support quiz modules. + */ +@Injectable() +export class AddonModQuizModuleHandler implements CoreCourseModuleHandler { + name = 'AddonModQuiz'; + modName = 'quiz'; + + constructor(private courseProvider: CoreCourseProvider) { } + + /** + * 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 { + return true; + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @param {number} sectionId The section ID. + * @return {CoreCourseModuleHandlerData} Data to render the module. + */ + getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + return { + icon: this.courseProvider.getModuleIconSrc('quiz'), + title: module.name, + class: 'addon-mod_quiz-handler', + showDownloadButton: true, + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + navCtrl.push('AddonModQuizIndexPage', {module: module, courseId: courseId}, options); + } + }; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any} The component to use, undefined if not found. + */ + getMainComponent(course: any, module: any): any { + return AddonModQuizIndexComponent; + } +} diff --git a/src/addon/mod/quiz/providers/prefetch-handler.ts b/src/addon/mod/quiz/providers/prefetch-handler.ts new file mode 100644 index 000000000..08b69cf66 --- /dev/null +++ b/src/addon/mod/quiz/providers/prefetch-handler.ts @@ -0,0 +1,451 @@ +// (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 { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; +import { AddonModQuizProvider } from './quiz'; +import { AddonModQuizHelperProvider } from './helper'; +import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate'; +import { AddonModQuizSyncProvider } from './quiz-sync'; +import { CoreConstants } from '@core/constants'; + +/** + * Handler to prefetch quizzes. + */ +@Injectable() +export class AddonModQuizPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + name = 'AddonModQuiz'; + modName = 'quiz'; + component = AddonModQuizProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^questions$|^attempts$/; + + protected syncProvider: AddonModQuizSyncProvider; // It will be injected later to prevent circular dependencies. + + constructor(protected injector: Injector, protected quizProvider: AddonModQuizProvider, + protected textUtils: CoreTextUtilsProvider, protected quizHelper: AddonModQuizHelperProvider, + protected accessRuleDelegate: AddonModQuizAccessRuleDelegate, protected questionHelper: CoreQuestionHelperProvider) { + super(injector); + } + + /** + * Download the module. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when all content is downloaded. + */ + download(module: any, courseId: number, dirPath?: string): Promise { + // Same implementation for download or prefetch. + return this.prefetch(module, courseId, false, dirPath); + } + + /** + * Get the download size of 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. + * @return {Promise<{size: number, total: boolean}>} Promise resolved with the size and a boolean indicating if it was able + * to calculate the total size. + */ + getDownloadSize(module: any, courseId: any, single?: boolean): Promise<{ size: number, total: boolean }> { + return Promise.resolve({ + size: -1, + total: false + }); + } + + /** + * Get list of files. If not defined, we'll assume they're in module.contents. + * + * @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. + * @return {Promise} Promise resolved with the list of files. + */ + getFiles(module: any, courseId: number, single?: boolean): Promise { + return Promise.resolve([]); + } + + /** + * Gather some preflight data for an attempt. This function will start a new attempt if needed. + * + * @param {any} quiz Quiz. + * @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation. + * @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt. + * @param {boolean} [askPreflight] Whether it should ask for preflight data if needed. + * @param {string} [modalTitle] Lang key of the title to set to preflight modal (e.g. 'addon.mod_quiz.startattempt'). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the preflight data. + */ + getPreflightData(quiz: any, accessInfo: any, attempt?: any, askPreflight?: boolean, title?: string, siteId?: string) + : Promise { + const preflightData = {}; + let promise; + + if (askPreflight) { + // We can ask preflight, check if it's needed and get the data. + promise = this.quizHelper.getAndCheckPreflightData( + quiz, accessInfo, preflightData, attempt, false, true, title, siteId); + } else { + // Get some fixed preflight data from access rules (data that doesn't require user interaction). + const rules = accessInfo.activerulenames; + + promise = this.accessRuleDelegate.getFixedPreflightData(rules, quiz, preflightData, attempt, true, siteId).then(() => { + if (!attempt) { + // We need to create a new attempt. + return this.quizProvider.startAttempt(quiz.id, preflightData, false, siteId); + } + }); + } + + return promise.then(() => { + return preflightData; + }); + } + + /** + * 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.quizProvider.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 { + // Invalidate the calls required to check if a quiz is downloadable. + const promises = []; + + promises.push(this.quizProvider.invalidateQuizData(courseId)); + promises.push(this.quizProvider.invalidateUserAttemptsForUser(module.instance)); + + return Promise.all(promises); + } + + /** + * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @return {boolean|Promise} Whether the module can be downloaded. The promise should never be rejected. + */ + isDownloadable(module: any, courseId: number): boolean | Promise { + const siteId = this.sitesProvider.getCurrentSiteId(); + + return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quiz) => { + if (quiz.allowofflineattempts !== 1 || quiz.hasquestions === 0) { + return false; + } + + // Not downloadable if we reached max attempts or the quiz has an unfinished attempt. + return this.quizProvider.getUserAttempts(quiz.id, undefined, true, false, false, siteId).then((attempts) => { + const isLastFinished = !attempts.length || this.quizProvider.isAttemptFinished(attempts[attempts.length - 1].state); + + return quiz.attempts === 0 || quiz.attempts > attempts.length || !isLastFinished; + }); + }); + } + + /** + * 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.quizProvider.isPluginEnabled(); + } + + /** + * 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. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when done. + */ + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + return this.prefetchPackage(module, courseId, single, this.prefetchQuiz.bind(this)); + } + + /** + * Prefetch a quiz. + * + * @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} siteId Site ID. + * @return {Promise} Promise resolved when done. + */ + protected prefetchQuiz(module: any, courseId: number, single: boolean, siteId: string): Promise { + let attempts: any[], + startAttempt = false, + quiz, + quizAccessInfo, + attemptAccessInfo, + preflightData; + + // Get quiz. + return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quizData) => { + quiz = quizData; + + const promises = [], + introFiles = this.getIntroFilesFromInstance(module, quiz); + + // Prefetch some quiz data. + promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { + quizAccessInfo = info; + })); + promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId)); + promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + attempts = atts; + })); + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId).then((info) => { + attemptAccessInfo = info; + })); + + promises.push(this.filepoolProvider.addFilesToQueue(siteId, introFiles, AddonModQuizProvider.COMPONENT, module.id)); + + return Promise.all(promises); + }).then(() => { + // Check if we need to start a new attempt. + let attempt = attempts[attempts.length - 1]; + if (!attempt || this.quizProvider.isAttemptFinished(attempt.state)) { + // Check if the user can attempt the quiz. + if (attemptAccessInfo.preventnewattemptreasons.length) { + return Promise.reject(this.textUtils.buildMessage(attemptAccessInfo.preventnewattemptreasons)); + } + + startAttempt = true; + attempt = undefined; + } + + // Get the preflight data. This function will also start a new attempt if needed. + return this.getPreflightData(quiz, quizAccessInfo, attempt, single, 'core.download', siteId); + + }).then((data) => { + preflightData = data; + + const promises = []; + + if (startAttempt) { + // Re-fetch user attempts since we created a new one. + promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + attempts = atts; + })); + + // Update the download time to prevent detecting the new attempt as an update. + promises.push(this.filepoolProvider.updatePackageDownloadTime(siteId, AddonModQuizProvider.COMPONENT, module.id) + .catch(() => { + // Ignore errors. + })); + } + + // Fetch attempt related data. + promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId)); + promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId)); + promises.push(this.quizProvider.getGradeFromGradebook(courseId, module.id, true, siteId).then((gradebookData) => { + if (typeof gradebookData.graderaw != 'undefined') { + return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); + } + }).catch(() => { + // Ignore errors. + })); + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt. + + return Promise.all(promises); + }).then(() => { + // We have quiz data, now we'll get specific data for each attempt. + const promises = []; + + attempts.forEach((attempt) => { + promises.push(this.prefetchAttempt(quiz, attempt, preflightData, siteId)); + }); + + return Promise.all(promises); + }).then(() => { + // If there's nothing to send, mark the quiz as synchronized. + // We don't return the promises because it should be fast and we don't want to block the user for this. + if (!this.syncProvider) { + this.syncProvider = this.injector.get(AddonModQuizSyncProvider); + } + + this.syncProvider.hasDataToSync(quiz.id, siteId).then((hasData) => { + if (!hasData) { + this.syncProvider.setSyncTime(quiz.id, siteId); + } + }); + }); + } + + /** + * Prefetch all WS data for an attempt. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight required data (like password). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the prefetch is finished. Data returned is not reliable. + */ + prefetchAttempt(quiz: any, attempt: any, preflightData: any, siteId?: string): Promise { + const pages = this.quizProvider.getPagesFromLayout(attempt.layout), + promises = [], + isSequential = this.quizProvider.isNavigationSequential(quiz); + + if (this.quizProvider.isAttemptFinished(attempt.state)) { + // Attempt is finished, get feedback and review data. + + const attemptGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false); + if (typeof attemptGrade != 'undefined') { + promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), true, siteId)); + } + + // Get the review for each page. + pages.forEach((page) => { + promises.push(this.quizProvider.getAttemptReview(attempt.id, page, true, siteId).catch(() => { + // Ignore failures, maybe the user can't review the attempt. + })); + }); + + // Get the review for all questions in same page. + promises.push(this.quizProvider.getAttemptReview(attempt.id, -1, true, siteId).then((data) => { + // Download the files inside the questions. + const questionPromises = []; + + data.questions.forEach((question) => { + questionPromises.push(this.questionHelper.prefetchQuestionFiles( + question, this.component, quiz.coursemodule, siteId)); + }); + + return Promise.all(questionPromises); + }, () => { + // Ignore failures, maybe the user can't review the attempt. + })); + } else { + + // Attempt not finished, get data needed to continue the attempt. + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, attempt.id, false, true, siteId)); + promises.push(this.quizProvider.getAttemptSummary(attempt.id, preflightData, false, true, false, siteId)); + + if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { + // Get data for each page. + pages.forEach((page) => { + if (isSequential && page < attempt.currentpage) { + // Sequential quiz, cannot get pages before the current one. + return; + } + + promises.push(this.quizProvider.getAttemptData(attempt.id, page, preflightData, false, true, siteId) + .then((data) => { + // Download the files inside the questions. + const questionPromises = []; + + data.questions.forEach((question) => { + questionPromises.push(this.questionHelper.prefetchQuestionFiles( + question, this.component, quiz.coursemodule, siteId)); + }); + + return Promise.all(questionPromises); + })); + }); + } + } + + return Promise.all(promises); + } + + /** + * Prefetches some data for a quiz and its last attempt. + * This function will NOT start a new attempt, it only reads data for the quiz and the last attempt. + * + * @param {any} quiz Quiz. + * @param {boolean} [askPreflight] Whether it should ask for preflight data if needed. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchQuizAndLastAttempt(quiz: any, askPreflight?: boolean, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + let attempts, + quizAccessInfo, + preflightData, + lastAttempt; + + // Get quiz data. + promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { + quizAccessInfo = info; + })); + promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId)); + promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId)); + promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId)); + promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + attempts = atts; + })); + promises.push(this.quizProvider.getGradeFromGradebook(quiz.course, quiz.coursemodule, true, siteId) + .then((gradebookData) => { + if (typeof gradebookData.graderaw != 'undefined') { + return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); + } + }).catch(() => { + // Ignore errors. + })); + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt. + + return Promise.all(promises).then(() => { + lastAttempt = attempts[attempts.length - 1]; + if (!lastAttempt) { + // No need to get attempt data, we don't need preflight data. + return; + } + + // Get the preflight data. + return this.getPreflightData(quiz, quizAccessInfo, lastAttempt, askPreflight, 'core.download', siteId); + + }).then((data) => { + preflightData = data; + + if (lastAttempt) { + // Get data for last attempt. + return this.prefetchAttempt(quiz, lastAttempt, preflightData, siteId); + } + }).then(() => { + // Prefetch finished, get current status to determine if we need to change it. + + return this.filepoolProvider.getPackageStatus(siteId, this.component, quiz.coursemodule); + }).then((status) => { + if (status !== CoreConstants.NOT_DOWNLOADED) { + // Quiz was downloaded, set the new status. + // If no attempts or last is finished we'll mark it as not downloaded to show download icon. + const isLastFinished = !lastAttempt || this.quizProvider.isAttemptFinished(lastAttempt.state), + newStatus = isLastFinished ? CoreConstants.NOT_DOWNLOADED : CoreConstants.DOWNLOADED; + + return this.filepoolProvider.storePackageStatus(siteId, newStatus, this.component, quiz.coursemodule); + } + }); + } +} diff --git a/src/addon/mod/quiz/providers/quiz-offline.ts b/src/addon/mod/quiz/providers/quiz-offline.ts new file mode 100644 index 000000000..34507bd55 --- /dev/null +++ b/src/addon/mod/quiz/providers/quiz-offline.ts @@ -0,0 +1,385 @@ +// (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 { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreQuestionProvider } from '@core/question/providers/question'; +import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate'; +import { AddonModQuizProvider } from './quiz'; +import { SQLiteDB } from '@classes/sqlitedb'; + +/** + * Service to handle offline quiz. + */ +@Injectable() +export class AddonModQuizOfflineProvider { + + protected logger; + + // Variables for database. + protected ATTEMPTS_TABLE = 'addon_mod_quiz_attempts'; + protected tablesSchema = [ + { + name: this.ATTEMPTS_TABLE, + columns: [ + { + name: 'id', // Attempt ID. + type: 'INTEGER', + primaryKey: true + }, + { + name: 'attempt', // Attempt number. + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'userid', + type: 'INTEGER' + }, + { + name: 'quizid', + type: 'INTEGER' + }, + { + name: 'currentpage', + type: 'INTEGER' + }, + { + name: 'timecreated', + type: 'INTEGER' + }, + { + name: 'timemodified', + type: 'INTEGER' + }, + { + name: 'finished', + type: 'INTEGER' + } + ] + } + ]; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider, + private questionProvider: CoreQuestionProvider, private translate: TranslateService, private utils: CoreUtilsProvider, + private behaviourDelegate: CoreQuestionBehaviourDelegate) { + this.logger = logger.getInstance('AddonModQuizOfflineProvider'); + + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Classify the answers in questions. + * + * @param {any} answers List of answers. + * @return {any} Object with the questions, the keys are the slot. Each question contains its answers. + */ + classifyAnswersInQuestions(answers: any): any { + const questionsWithAnswers = {}; + + // Classify the answers in each question. + for (const name in answers) { + const slot = this.questionProvider.getQuestionSlotFromName(name), + nameWithoutPrefix = this.questionProvider.removeQuestionPrefix(name); + + if (!questionsWithAnswers[slot]) { + questionsWithAnswers[slot] = { + answers: {}, + prefix: name.substr(0, name.indexOf(nameWithoutPrefix)) + }; + } + questionsWithAnswers[slot].answers[nameWithoutPrefix] = answers[name]; + } + + return questionsWithAnswers; + } + + /** + * Given a list of questions with answers classified in it (@see AddonModQuizOfflineProvider.classifyAnswersInQuestions), + * returns a list of answers (including prefix in the name). + * + * @param {any} questions Questions. + * @return {any} Answers. + */ + extractAnswersFromQuestions(questions: any): any { + const answers = {}; + + for (const slot in questions) { + const question = questions[slot]; + + for (const name in question.answers) { + answers[question.prefix + name] = question.answers[name]; + } + } + + return answers; + } + + /** + * Get all the offline attempts in a certain site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the offline attempts. + */ + getAllAttempts(siteId?: string): Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + return db.getAllRecords(this.ATTEMPTS_TABLE); + }); + } + + /** + * Retrieve an attempt answers from site DB. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the answers. + */ + getAttemptAnswers(attemptId: number, siteId?: string): Promise { + return this.questionProvider.getAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId); + } + + /** + * Retrieve an attempt from site DB. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the attempt. + */ + getAttemptById(attemptId: number, siteId?: string): Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + return db.getRecord(this.ATTEMPTS_TABLE, {id: attemptId}); + }); + } + + /** + * Retrieve an attempt from site DB. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User ID. If not defined, user current site's user. + * @return {Promise} Promise resolved with the attempts. + */ + getQuizAttempts(quizId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.getDb().getRecords(this.ATTEMPTS_TABLE, {quizid: quizId, userid: userId}); + }); + } + + /** + * Load local state in the questions. + * + * @param {number} attemptId Attempt ID. + * @param {any[]} questions List of questions. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + loadQuestionsLocalStates(attemptId: number, questions: any[], siteId?: string): Promise { + const promises = []; + + questions.forEach((question) => { + promises.push(this.questionProvider.getQuestion(AddonModQuizProvider.COMPONENT, attemptId, question.slot, siteId) + .then((q) => { + + const state = this.questionProvider.getState(q.state); + question.state = q.state; + question.status = this.translate.instant('core.question.' + state.status); + }).catch(() => { + // Question not found. + })); + }); + + return Promise.all(promises).then(() => { + return questions; + }); + } + + /** + * Process an attempt, saving its data. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} questions Object with the questions of the quiz. The keys should be the question slot. + * @param {any} data Data to save. + * @param {boolean} [finish] Whether to finish the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + processAttempt(quiz: any, attempt: any, questions: any, data: any, finish?: boolean, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const now = this.timeUtils.timestamp(); + let db: SQLiteDB; + + return this.sitesProvider.getSiteDb(siteId).then((siteDb) => { + db = siteDb; + + // Check if an attempt already exists. + return this.getAttemptById(attempt.id, siteId).catch(() => { + // Attempt doesn't exist, create a new entry. + return { + quizid: quiz.id, + userid: attempt.userid, + id: attempt.id, + courseid: quiz.course, + timecreated: now, + attempt: attempt.attempt, + currentpage: attempt.currentpage + }; + }); + }).then((entry) => { + // Save attempt in DB. + entry.timemodified = now; + entry.finished = finish ? 1 : 0; + + return db.insertRecord(this.ATTEMPTS_TABLE, entry); + }).then(() => { + // Attempt has been saved, now we need to save the answers. + return this.saveAnswers(quiz, attempt, questions, data, now, siteId); + }); + } + + /** + * Remove an attempt and its answers from local DB. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + removeAttemptAndAnswers(attemptId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + + // Remove stored answers and questions. + promises.push(this.questionProvider.removeAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId)); + promises.push(this.questionProvider.removeAttemptQuestions(AddonModQuizProvider.COMPONENT, attemptId, siteId)); + + // Remove the attempt. + promises.push(this.sitesProvider.getSiteDb(siteId).then((db) => { + return db.deleteRecords(this.ATTEMPTS_TABLE, {id: attemptId}); + })); + + return Promise.all(promises); + } + + /** + * Remove a question and its answers from local DB. + * + * @param {number} attemptId Attempt ID. + * @param {number} slot Question slot. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when finished. + */ + removeQuestionAndAnswers(attemptId: number, slot: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + + promises.push(this.questionProvider.removeQuestion(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId)); + promises.push(this.questionProvider.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId)); + + return Promise.all(promises); + } + + /** + * Save an attempt's answers and calculate state for questions modified. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} questions Object with the questions of the quiz. The keys should be the question slot. + * @param {any} answers Answers to save. + * @param {number} [timeMod] Time modified to set in the answers. If not defined, current time. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + saveAnswers(quiz: any, attempt: any, questions: any, answers: any, timeMod?: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + timeMod = timeMod || this.timeUtils.timestamp(); + + const questionsWithAnswers = {}, + newStates = {}; + let promises = []; + + // Classify the answers in each question. + for (const name in answers) { + const slot = this.questionProvider.getQuestionSlotFromName(name), + nameWithoutPrefix = this.questionProvider.removeQuestionPrefix(name); + + if (questions[slot]) { + if (!questionsWithAnswers[slot]) { + questionsWithAnswers[slot] = questions[slot]; + questionsWithAnswers[slot].answers = {}; + } + questionsWithAnswers[slot].answers[nameWithoutPrefix] = answers[name]; + } + } + + // First determine the new state of each question. We won't save the new state yet. + for (const slot in questionsWithAnswers) { + const question = questionsWithAnswers[slot]; + + promises.push(this.behaviourDelegate.determineNewState( + quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT, attempt.id, question, siteId).then((state) => { + // Check if state has changed. + if (state && state.name != question.state) { + newStates[question.slot] = state.name; + } + })); + } + + return Promise.all(promises).then(() => { + // Now save the answers. + return this.questionProvider.saveAnswers(AddonModQuizProvider.COMPONENT, quiz.id, attempt.id, attempt.userid, + answers, timeMod, siteId); + }).then(() => { + // Answers have been saved, now we can save the questions with the states. + promises = []; + + for (const slot in newStates) { + const question = questionsWithAnswers[slot]; + + promises.push(this.questionProvider.saveQuestion(AddonModQuizProvider.COMPONENT, quiz.id, attempt.id, + attempt.userid, question, newStates[slot], siteId)); + } + + return this.utils.allPromises(promises).catch((err) => { + // Ignore errors when saving question state. + this.logger.error('Error saving question state', err); + }); + }); + } + + /** + * Set attempt's current page. + * + * @param {number} attemptId Attempt ID. + * @param {number} page Page to set. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + setAttemptCurrentPage(attemptId: number, page: number, siteId?: string): Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + return db.updateRecords(this.ATTEMPTS_TABLE, {currentpage: page}, {id: attemptId}); + }); + } +} diff --git a/src/addon/mod/quiz/providers/quiz-sync.ts b/src/addon/mod/quiz/providers/quiz-sync.ts new file mode 100644 index 000000000..901850d1c --- /dev/null +++ b/src/addon/mod/quiz/providers/quiz-sync.ts @@ -0,0 +1,404 @@ +// (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 { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreQuestionProvider } from '@core/question/providers/question'; +import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { AddonModQuizProvider } from './quiz'; +import { AddonModQuizOfflineProvider } from './quiz-offline'; +import { AddonModQuizPrefetchHandler } from './prefetch-handler'; + +/** + * Data returned by a quiz sync. + */ +export interface AddonModQuizSyncResult { + /** + * List of warnings. + * @type {string[]} + */ + warnings: string[]; + + /** + * Whether an attempt was finished in the site due to the sync, + * @type {boolean} + */ + attemptFinished: boolean; +} + +/** + * Service to sync quizzes. + */ +@Injectable() +export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_mod_quiz_autom_synced'; + static SYNC_TIME = 300000; + + protected componentTranslate: string; + + constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, + syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, + courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, + private quizProvider: AddonModQuizProvider, private quizOfflineProvider: AddonModQuizOfflineProvider, + private prefetchHandler: AddonModQuizPrefetchHandler, private questionProvider: CoreQuestionProvider, + private questionDelegate: CoreQuestionDelegate) { + + super('AddonModQuizSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); + + this.componentTranslate = courseProvider.translateModuleName('quiz'); + } + + /** + * Finish a sync process: remove offline data if needed, prefetch quiz data, set sync time and return the result. + * + * @param {string} siteId Site ID. + * @param {any} quiz Quiz. + * @param {number} courseId Course ID. + * @param {string[]} warnings List of warnings generated by the sync. + * @param {number} [attemptId] Last attempt ID. + * @param {any} [offlineAttempt] Offline attempt synchronized, if any. + * @param {any} [onlineAttempt] Online data for the offline attempt. + * @param {boolean} [removeAttempt] Whether the offline data should be removed. + * @param {boolean} [updated] Whether some data was sent to the site. + * @return {Promise} Promise resolved on success. + */ + protected finishSync(siteId: string, quiz: any, courseId: number, warnings: string[], attemptId?: number, offlineAttempt?: any, + onlineAttempt?: any, removeAttempt?: boolean, updated?: boolean): Promise { + + // Invalidate the data for the quiz and attempt. + return this.quizProvider.invalidateAllQuizData(quiz.id, courseId, attemptId, siteId).catch(() => { + // Ignore errors. + }).then(() => { + if (removeAttempt && attemptId) { + return this.quizOfflineProvider.removeAttemptAndAnswers(attemptId, siteId); + } + }).then(() => { + if (updated) { + // Data has been sent. Update prefetched data. + return this.prefetchHandler.prefetchQuizAndLastAttempt(quiz, false, siteId); + } + }).then(() => { + return this.setSyncTime(quiz.id, siteId).catch(() => { + // Ignore errors. + }); + }).then(() => { + // Check if online attempt was finished because of the sync. + if (onlineAttempt && !this.quizProvider.isAttemptFinished(onlineAttempt.state)) { + // Attempt wasn't finished at start. Check if it's finished now. + return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => { + // Search the attempt. + for (const i in attempts) { + const attempt = attempts[i]; + + if (attempt.id == onlineAttempt.id) { + return this.quizProvider.isAttemptFinished(attempt.state); + } + } + + return false; + }); + } + + return false; + }).then((attemptFinished) => { + return { + warnings: warnings, + attemptFinished: attemptFinished + }; + }); + } + + /** + * Check if a quiz has data to synchronize. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether it has data to sync. + */ + hasDataToSync(quizId: number, siteId?: string): Promise { + return this.quizOfflineProvider.getQuizAttempts(quizId, siteId).then((attempts) => { + return !!attempts.length; + }).catch(() => { + return false; + }); + } + + /** + * Try to synchronize all the quizzes in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllQuizzes(siteId?: string): Promise { + return this.syncOnSites('all quizzes', this.syncAllQuizzesFunc.bind(this), [], siteId); + } + + /** + * Sync all quizzes on a site. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllQuizzesFunc(siteId?: string): Promise { + // Get all offline attempts. + return this.quizOfflineProvider.getAllAttempts(siteId).then((attempts) => { + const quizzes = [], + ids = [], // To prevent duplicates. + promises = []; + + // Get the IDs of all the quizzes that have something to be synced. + attempts.forEach((attempt) => { + if (ids.indexOf(attempt.quizid) == -1) { + ids.push(attempt.quizid); + + quizzes.push({ + id: attempt.quizid, + courseid: attempt.courseid + }); + } + }); + + // Sync all quizzes that haven't been synced for a while and that aren't attempted right now. + quizzes.forEach((quiz) => { + if (!this.syncProvider.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) { + + // Quiz not blocked, try to synchronize it. + promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, false, siteId).then((quiz) => { + return this.syncQuizIfNeeded(quiz, false, siteId).then((data) => { + if (data && data.warnings && data.warnings.length) { + // Store the warnings to show them when the user opens the quiz. + return this.setSyncWarnings(quiz.id, data.warnings, siteId).then(() => { + return data; + }); + } + + return data; + }).then((data) => { + if (typeof data != 'undefined') { + // Sync successful. Send event. + this.eventsProvider.trigger(AddonModQuizSyncProvider.AUTO_SYNCED, { + quizId: quiz.id, + attemptFinished: data.attemptFinished, + warnings: data.warnings + }, siteId); + } + }); + })); + } + }); + + return Promise.all(promises); + }); + } + + /** + * Sync a quiz only if a certain time has passed since the last time. + * + * @param {any} quiz Quiz. + * @param {boolean} [askPreflight] Whether we should ask for preflight data if needed. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the quiz is synced or if it doesn't need to be synced. + */ + syncQuizIfNeeded(quiz: any, askPreflight?: boolean, siteId?: string): Promise { + return this.isSyncNeeded(quiz.id, siteId).then((needed) => { + if (needed) { + return this.syncQuiz(quiz, askPreflight, siteId); + } + }); + } + + /** + * Try to synchronize a quiz. + * The promise returned will be resolved with an array with warnings if the synchronization is successful. + * + * @param {any} quiz Quiz. + * @param {boolean} [askPreflight] Whether we should ask for preflight data if needed. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved in success. + */ + syncQuiz(quiz: any, askPreflight?: boolean, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const warnings = [], + courseId = quiz.course; + let syncPromise, + preflightData; + + if (this.isSyncing(quiz.id, siteId)) { + // There's already a sync ongoing for this quiz, return the promise. + return this.getOngoingSync(quiz.id, siteId); + } + + // Verify that quiz isn't blocked. + if (this.syncProvider.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) { + this.logger.debug('Cannot sync quiz ' + quiz.id + ' because it is blocked.'); + + return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + } + + this.logger.debug('Try to sync quiz ' + quiz.id + ' in site ' + siteId); + + // Get all the offline attempts for the quiz. + syncPromise = this.quizOfflineProvider.getQuizAttempts(quiz.id, siteId).then((attempts) => { + // Should return 0 or 1 attempt. + if (!attempts.length) { + return this.finishSync(siteId, quiz, courseId, warnings); + } + + const offlineAttempt = attempts.pop(); + + // Now get the list of online attempts to make sure this attempt exists and isn't finished. + return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((attempts) => { + const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined; + let onlineAttempt; + + // Search the attempt we retrieved from offline. + for (const i in attempts) { + const attempt = attempts[i]; + + if (attempt.id == offlineAttempt.id) { + onlineAttempt = attempt; + break; + } + } + + if (!onlineAttempt || this.quizProvider.isAttemptFinished(onlineAttempt.state)) { + // Attempt not found or it's finished in online. Discard it. + warnings.push(this.translate.instant('addon.mod_quiz.warningattemptfinished')); + + return this.finishSync(siteId, quiz, courseId, warnings, lastAttemptId, offlineAttempt, onlineAttempt, true); + } + + // Get the data stored in offline. + return this.quizOfflineProvider.getAttemptAnswers(offlineAttempt.id, siteId).then((answersList) => { + + if (!answersList.length) { + // No answers stored, finish. + return this.finishSync(siteId, quiz, courseId, warnings, lastAttemptId, offlineAttempt, onlineAttempt, + true); + } + + const answers = this.questionProvider.convertAnswersArrayToObject(answersList), + offlineQuestions = this.quizOfflineProvider.classifyAnswersInQuestions(answers); + let finish; + + // We're going to need preflightData, get it. + return this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { + + return this.prefetchHandler.getPreflightData(quiz, info, onlineAttempt, askPreflight, + 'core.settings.synchronization', siteId); + }).then((data) => { + preflightData = data; + + // Now get the online questions data. + const pages = this.quizProvider.getPagesFromLayoutAndQuestions(onlineAttempt.layout, offlineQuestions); + + return this.quizProvider.getAllQuestionsData(quiz, onlineAttempt, preflightData, pages, false, true, + siteId); + }).then((onlineQuestions) => { + + // Validate questions, discarding the offline answers that can't be synchronized. + return this.validateQuestions(onlineAttempt.id, onlineQuestions, offlineQuestions, siteId); + }).then((discardedData) => { + + // Get the answers to send. + const answers = this.quizOfflineProvider.extractAnswersFromQuestions(offlineQuestions); + finish = offlineAttempt.finished && !discardedData; + + if (discardedData) { + if (offlineAttempt.finished) { + warnings.push(this.translate.instant('addon.mod_quiz.warningdatadiscardedfromfinished')); + } else { + warnings.push(this.translate.instant('addon.mod_quiz.warningdatadiscarded')); + } + } + + return this.quizProvider.processAttempt(quiz, onlineAttempt, answers, preflightData, finish, false, false, + siteId); + }).then(() => { + + // Answers sent, now set the current page if the attempt isn't finished. + if (!finish) { + return this.quizProvider.logViewAttempt(onlineAttempt.id, offlineAttempt.currentpage, preflightData, + false).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + + // Data sent. Finish the sync. + return this.finishSync(siteId, quiz, courseId, warnings, lastAttemptId, offlineAttempt, onlineAttempt, + true, true); + }); + }); + }); + }); + + return this.addOngoingSync(quiz.id, syncPromise, siteId); + } + + /** + * Validate questions, discarding the offline answers that can't be synchronized. + * + * @param {number} attemptId Attempt ID. + * @param {any} onlineQuestions Online questions + * @param {any} offlineQuestions Offline questions. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if some offline data was discarded, false otherwise. + */ + validateQuestions(attemptId: number, onlineQuestions: any, offlineQuestions: any, siteId?: string): Promise { + const promises = []; + let discardedData = false; + + for (const slot in offlineQuestions) { + const offlineQuestion = offlineQuestions[slot], + onlineQuestion = onlineQuestions[slot], + offlineSequenceCheck = offlineQuestion.answers[':sequencecheck']; + + if (onlineQuestion) { + + // We found the online data for the question, validate that the sequence check is ok. + if (!this.questionDelegate.validateSequenceCheck(onlineQuestion, offlineSequenceCheck)) { + // Sequence check is not valid, remove the offline data. + discardedData = true; + promises.push(this.quizOfflineProvider.removeQuestionAndAnswers(attemptId, Number(slot), siteId)); + delete offlineQuestions[slot]; + } else { + // Sequence check is valid. Use the online one to prevent synchronization errors. + offlineQuestion.answers[':sequencecheck'] = onlineQuestion.sequencecheck; + } + } else { + // Online question not found, it can happen for 2 reasons: + // 1- It's a sequential quiz and the question is in a page already passed. + // 2- Quiz layout has changed (shouldn't happen since it's blocked if there are attempts). + discardedData = true; + promises.push(this.quizOfflineProvider.removeQuestionAndAnswers(attemptId, Number(slot), siteId)); + delete offlineQuestions[slot]; + } + } + + return Promise.all(promises).then(() => { + return discardedData; + }); + } +} diff --git a/src/addon/mod/quiz/providers/quiz.ts b/src/addon/mod/quiz/providers/quiz.ts new file mode 100644 index 000000000..859510c25 --- /dev/null +++ b/src/addon/mod/quiz/providers/quiz.ts @@ -0,0 +1,1816 @@ +// (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 { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; +import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate'; +import { AddonModQuizOfflineProvider } from './quiz-offline'; +import * as moment from 'moment'; + +/** + * Service that provides some features for quiz. + */ +@Injectable() +export class AddonModQuizProvider { + static COMPONENT = 'mmaModQuiz'; + static ATTEMPT_FINISHED_EVENT = 'addon_mod_quiz_attempt_finished'; + + // Grade methods. + static GRADEHIGHEST = 1; + static GRADEAVERAGE = 2; + static ATTEMPTFIRST = 3; + static ATTEMPTLAST = 4; + + // Question options. + static QUESTION_OPTIONS_MAX_ONLY = 1; + static QUESTION_OPTIONS_MARK_AND_MAX = 2; + + // Attempt state. + static ATTEMPT_IN_PROGRESS = 'inprogress'; + static ATTEMPT_OVERDUE = 'overdue'; + static ATTEMPT_FINISHED = 'finished'; + static ATTEMPT_ABANDONED = 'abandoned'; + + // Show the countdown timer if there is less than this amount of time left before the the quiz close date. + static QUIZ_SHOW_TIME_BEFORE_DEADLINE = 3600; + + protected ROOT_CACHE_KEY = 'mmaModQuiz:'; + protected logger; + protected div = document.createElement('div'); // A div element to search in HTML code. + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, + private translate: TranslateService, private textUtils: CoreTextUtilsProvider, + private gradesHelper: CoreGradesHelperProvider, private questionDelegate: CoreQuestionDelegate, + private filepoolProvider: CoreFilepoolProvider, private timeUtils: CoreTimeUtilsProvider, + private accessRulesDelegate: AddonModQuizAccessRuleDelegate, private quizOfflineProvider: AddonModQuizOfflineProvider) { + this.logger = logger.getInstance('AddonModQuizProvider'); + } + + /** + * Formats a grade to be displayed. + * + * @param {number} grade Grade. + * @param {number} decimals Decimals to use. + * @return {string} Grade to display. + */ + formatGrade(grade: number, decimals: number): string { + if (typeof grade == 'undefined' || grade == -1 || grade === null) { + return this.translate.instant('addon.mod_quiz.notyetgraded'); + } + + return this.utils.formatFloat(this.textUtils.roundToDecimals(grade, decimals)); + } + + /** + * Get attempt questions. Returns all of them or just the ones in certain pages. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight required data (like password). + * @param {number[]} [pages] List of pages to get. If not defined, all pages. + * @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache] Whether 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 questions. + */ + getAllQuestionsData(quiz: any, attempt: any, preflightData: any, pages?: number[], offline?: boolean, ignoreCache?: boolean, + siteId?: string): Promise { + + const promises = [], + questions = {}, + isSequential = this.isNavigationSequential(quiz); + + if (!pages) { + pages = this.getPagesFromLayout(attempt.layout); + } + + pages.forEach((page) => { + if (isSequential && page < attempt.currentpage) { + // Sequential quiz, cannot get pages before the current one. + return; + } + + // Get the questions in the page. + promises.push(this.getAttemptData(attempt.id, page, preflightData, offline, ignoreCache, siteId).then((data) => { + // Add the questions to the result object. + data.questions.forEach((question) => { + questions[question.slot] = question; + }); + })); + }); + + return Promise.all(promises).then(() => { + return questions; + }); + } + + /** + * Get cache key for get attempt access information WS calls. + * + * @param {number} quizId Quiz ID. + * @param {number} attemptId Attempt ID. + * @return {string} Cache key. + */ + protected getAttemptAccessInformationCacheKey(quizId: number, attemptId: number): string { + return this.getAttemptAccessInformationCommonCacheKey(quizId) + ':' + attemptId; + } + + /** + * Get common cache key for get attempt access information WS calls. + * + * @param {number} quizId Quiz ID. + * @return {string} Cache key. + */ + protected getAttemptAccessInformationCommonCacheKey(quizId: number): string { + return this.ROOT_CACHE_KEY + 'attemptAccessInformation:' + quizId; + } + + /** + * Get access information for an attempt. + * + * @param {number} quizId Quiz ID. + * @param {number} attemptId Attempt ID. 0 for user's last attempt. + * @param {boolean} offline Whether it should return cached data. Has priority over ignoreCache. + * @param {boolean} ignoreCache Whether 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 access information. + */ + getAttemptAccessInformation(quizId: number, attemptId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) + : Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + quizid: quizId, + attemptid: attemptId + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_attempt_access_information', params, preSets); + }); + } + + /** + * Get cache key for get attempt data WS calls. + * + * @param {number} attemptId Attempt ID. + * @param {number} page Page. + * @return {string} Cache key. + */ + protected getAttemptDataCacheKey(attemptId: number, page: number): string { + return this.getAttemptDataCommonCacheKey(attemptId) + ':' + page; + } + + /** + * Get common cache key for get attempt data WS calls. + * + * @param {number} attemptId Attempt ID. + * @return {string} Cache key. + */ + protected getAttemptDataCommonCacheKey(attemptId: number): string { + return this.ROOT_CACHE_KEY + 'attemptData:' + attemptId; + } + + /** + * Get an attempt's data. + * + * @param {number} attemptId Attempt ID. + * @param {number} page Page number. + * @param {any} preflightData Preflight required data (like password). + * @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache] Whether 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 attempt data. + */ + getAttemptData(attemptId: number, page: number, preflightData: any, offline?: boolean, ignoreCache?: boolean, siteId?: string) + : Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + attemptid: attemptId, + page: page, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true) + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getAttemptDataCacheKey(attemptId, page) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_attempt_data', params, preSets); + }); + } + + /** + * Get an attempt's due date. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @return {number} Attempt's due date, 0 if no due date or invalid data. + */ + getAttemptDueDate(quiz: any, attempt: any): number { + const deadlines = []; + + if (quiz.timelimit) { + deadlines.push(parseInt(attempt.timestart, 10) + parseInt(quiz.timelimit, 10)); + } + if (quiz.timeclose) { + deadlines.push(parseInt(quiz.timeclose, 10)); + } + + if (!deadlines.length) { + return 0; + } + + // Get min due date. + const dueDate = Math.min.apply(null, deadlines); + if (!dueDate) { + return 0; + } + + switch (attempt.state) { + case AddonModQuizProvider.ATTEMPT_IN_PROGRESS: + return dueDate * 1000; + + case AddonModQuizProvider.ATTEMPT_OVERDUE: + return (dueDate + parseInt(quiz.graceperiod, 10)) * 1000; + + default: + this.logger.warn('Unexpected state when getting due date: ' + attempt.state); + + return 0; + } + } + + /** + * Get an attempt's warning because of due date. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @return {string} Attempt's warning, undefined if no due date. + */ + getAttemptDueDateWarning(quiz: any, attempt: any): string { + const dueDate = this.getAttemptDueDate(quiz, attempt); + + if (attempt.state === AddonModQuizProvider.ATTEMPT_OVERDUE) { + return this.translate.instant('addon.mod_quiz.overduemustbesubmittedby', {$a: moment(dueDate).format('LLL')}); + } else if (dueDate) { + return this.translate.instant('addon.mod_quiz.mustbesubmittedby', {$a: moment(dueDate).format('LLL')}); + } + } + + /** + * Turn attempt's state into a readable state, including some extra data depending on the state. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @return {string[]} List of state sentences. + */ + getAttemptReadableState(quiz: any, attempt: any): string[] { + if (attempt.finishedOffline) { + return [this.translate.instant('addon.mod_quiz.finishnotsynced')]; + } + + switch (attempt.state) { + case AddonModQuizProvider.ATTEMPT_IN_PROGRESS: + return [this.translate.instant('addon.mod_quiz.stateinprogress')]; + + case AddonModQuizProvider.ATTEMPT_OVERDUE: + const sentences = [], + dueDate = this.getAttemptDueDate(quiz, attempt); + + sentences.push(this.translate.instant('addon.mod_quiz.stateoverdue')); + + if (dueDate) { + sentences.push(this.translate.instant('addon.mod_quiz.stateoverduedetails', + {$a: moment(dueDate).format('LLL')})); + } + + return sentences; + + case AddonModQuizProvider.ATTEMPT_FINISHED: + return [ + this.translate.instant('addon.mod_quiz.statefinished'), + this.translate.instant('addon.mod_quiz.statefinisheddetails', + {$a: moment(attempt.timefinish * 1000).format('LLL')}) + ]; + + case AddonModQuizProvider.ATTEMPT_ABANDONED: + return [this.translate.instant('addon.mod_quiz.stateabandoned')]; + + default: + return []; + } + } + + /** + * Turn attempt's state into a readable state name, without any more data. + * + * @param {string} state State. + * @return {string} Readable state name. + */ + getAttemptReadableStateName(state: string): string { + switch (state) { + case AddonModQuizProvider.ATTEMPT_IN_PROGRESS: + return this.translate.instant('addon.mod_quiz.stateinprogress'); + + case AddonModQuizProvider.ATTEMPT_OVERDUE: + return this.translate.instant('addon.mod_quiz.stateoverdue'); + + case AddonModQuizProvider.ATTEMPT_FINISHED: + return this.translate.instant('addon.mod_quiz.statefinished'); + + case AddonModQuizProvider.ATTEMPT_ABANDONED: + return this.translate.instant('addon.mod_quiz.stateabandoned'); + + default: + return ''; + } + } + + /** + * Get cache key for get attempt review WS calls. + * + * @param {number} attemptId Attempt ID. + * @param {number} page Page. + * @return {string} Cache key. + */ + protected getAttemptReviewCacheKey(attemptId: number, page: number): string { + return this.getAttemptReviewCommonCacheKey(attemptId) + ':' + page; + } + + /** + * Get common cache key for get attempt review WS calls. + * + * @param {number} attemptId Attempt ID. + * @return {string} Cache key. + */ + protected getAttemptReviewCommonCacheKey(attemptId: number): string { + return this.ROOT_CACHE_KEY + 'attemptReview:' + attemptId; + } + + /** + * Get an attempt's review. + * + * @param {number} attemptId Attempt ID. + * @param {number} [page] Page number. If not defined, return all the questions in all the pages. + * @param {boolean} [ignoreCache] Whether 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 attempt review. + */ + getAttemptReview(attemptId: number, page?: number, ignoreCache?: boolean, siteId?: string): Promise { + if (typeof page == 'undefined') { + page = -1; + } + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + attemptid: attemptId, + page: page + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getAttemptReviewCacheKey(attemptId, page) + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_attempt_review', params, preSets); + }); + } + + /** + * Get cache key for get attempt summary WS calls. + * + * @param {number} attemptId Attempt ID. + * @return {string} Cache key. + */ + protected getAttemptSummaryCacheKey(attemptId: number): string { + return this.ROOT_CACHE_KEY + 'attemptSummary:' + attemptId; + } + + /** + * Get an attempt's summary. + * + * @param {number} attemptId Attempt ID. + * @param {any} preflightData Preflight required data (like password). + * @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). + * @param {boolean} [loadLocal] Whether it should load local state for each question. Only applicable if offline=true. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the list of questions for the attempt summary. + */ + getAttemptSummary(attemptId: number, preflightData: any, offline?: boolean, ignoreCache?: boolean, loadLocal?: boolean, + siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + attemptid: attemptId, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true) + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getAttemptSummaryCacheKey(attemptId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_attempt_summary', params, preSets).then((response) => { + if (response && response.questions) { + if (offline && loadLocal) { + return this.quizOfflineProvider.loadQuestionsLocalStates(attemptId, response.questions, site.getId()); + } + + return response.questions; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get cache key for get combined review options WS calls. + * + * @param {number} quizId Quiz ID. + * @param {number} userId User ID. + * @return {string} Cache key. + */ + protected getCombinedReviewOptionsCacheKey(quizId: number, userId: number): string { + return this.getCombinedReviewOptionsCommonCacheKey(quizId) + ':' + userId; + } + + /** + * Get common cache key for get combined review options WS calls. + * + * @param {number} quizId Quiz ID. + * @return {string} Cache key. + */ + protected getCombinedReviewOptionsCommonCacheKey(quizId: number): string { + return this.ROOT_CACHE_KEY + 'combinedReviewOptions:' + quizId; + } + + /** + * Get a quiz combined review options. + * + * @param {number} quizId Quiz ID. + * @param {boolean} [ignoreCache] Whether 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 {number} [userId] User ID. If not defined use site's current user. + * @return {Promise}Promise resolved with the combined review options. + */ + getCombinedReviewOptions(quizId: number, ignoreCache?: boolean, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const params = { + quizid: quizId, + userid: userId + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId) + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_combined_review_options', params, preSets).then((response) => { + if (response && response.someoptions && response.alloptions) { + // Convert the arrays to objects with name -> value. + response.someoptions = this.utils.objectToKeyValueMap(response.someoptions, 'name', 'value'); + response.alloptions = this.utils.objectToKeyValueMap(response.alloptions, 'name', 'value'); + + return response; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get cache key for get feedback for grade WS calls. + * + * @param {number} quizId Quiz ID. + * @param {number} grade Grade. + * @return {string} Cache key. + */ + protected getFeedbackForGradeCacheKey(quizId: number, grade: number): string { + return this.getFeedbackForGradeCommonCacheKey(quizId) + ':' + grade; + } + + /** + * Get common cache key for get feedback for grade WS calls. + * + * @param {number} quizId Quiz ID. + * @return {string} Cache key. + */ + protected getFeedbackForGradeCommonCacheKey(quizId: number): string { + return this.ROOT_CACHE_KEY + 'feedbackForGrade:' + quizId; + } + + /** + * Get the feedback for a certain grade. + * + * @param {number} quizId Quiz ID. + * @param {number} grade Grade. + * @param {boolean} [ignoreCache] Whether 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 feedback. + */ + getFeedbackForGrade(quizId: number, grade: number, ignoreCache?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + quizid: quizId, + grade: grade + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade) + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_quiz_feedback_for_grade', params, preSets); + }); + } + + /** + * Determine the correct number of decimal places required to format a grade. + * Based on Moodle's quiz_get_grade_format. + * + * @param {any} quiz Quiz. + * @return {number} Number of decimals. + */ + getGradeDecimals(quiz: any): number { + if (typeof quiz.questiondecimalpoints == 'undefined') { + quiz.questiondecimalpoints = -1; + } + + if (quiz.questiondecimalpoints == -1) { + return quiz.decimalpoints; + } + + return quiz.questiondecimalpoints; + } + + /** + * Gets a quiz grade and feedback from the gradebook. + * + * @param {number} courseId Course ID. + * @param {number} moduleId Quiz module ID. + * @param {boolean} [ignoreCache] Whether 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 {number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved with an object containing the grade and the feedback. + */ + getGradeFromGradebook(courseId: number, moduleId: number, ignoreCache?: boolean, siteId?: string, userId?: number) + : Promise { + + return this.gradesHelper.getGradeModuleItems(courseId, moduleId, userId, null, siteId, ignoreCache).then((items) => { + return items.shift(); + }); + } + + /** + * Given a list of attempts, returns the last finished attempt. + * + * @param {any[]} attempts Attempts. + * @return {any} Last finished attempt. + */ + getLastFinishedAttemptFromList(attempts: any[]): any { + if (attempts && attempts.length) { + for (let i = attempts.length - 1; i >= 0; i--) { + const attempt = attempts[i]; + + if (this.isAttemptFinished(attempt.state)) { + return attempt; + } + } + } + } + + /** + * Given a list of questions, check if the quiz can be submitted. + * Will return an array with the messages to prevent the submit. Empty array if quiz can be submitted. + * + * @param {any[]} questions Questions. + * @return {string[]} List of prevent submit messages. Empty array if quiz can be submitted. + */ + getPreventSubmitMessages(questions: any[]): string[] { + const messages = []; + + questions.forEach((question) => { + let message = this.questionDelegate.getPreventSubmitMessage(question); + if (message) { + message = this.translate.instant(message); + messages.push(this.translate.instant('core.question.questionmessage', {$a: question.slot, $b: message})); + } + }); + + return messages; + } + + /** + * Get cache key for quiz data WS calls. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getQuizDataCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'quiz:' + courseId; + } + + /** + * Get a Quiz with key=value. If more than one is found, only the first will be returned. + * + * @param {number} courseId Course ID. + * @param {string} key Name of the property to check. + * @param {any} value Value to search. + * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the Quiz is retrieved. + */ + protected getQuizByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseids: [courseId] + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getQuizDataCacheKey(courseId) + }; + + if (forceCache) { + preSets.omitExpires = true; + } + + return site.read('mod_quiz_get_quizzes_by_courses', params, preSets).then((response) => { + if (response && response.quizzes) { + // Search the quiz. + for (const i in response.quizzes) { + const quiz = response.quizzes[i]; + if (quiz[key] == value) { + return quiz; + } + } + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get a quiz by module ID. + * + * @param {number} courseId Course ID. + * @param {number} cmId Course module ID. + * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the quiz is retrieved. + */ + getQuiz(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { + return this.getQuizByField(courseId, 'coursemodule', cmId, forceCache, siteId); + } + + /** + * Get a quiz by quiz ID. + * + * @param {number} courseId Course ID. + * @param {number} id Quiz ID. + * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the quiz is retrieved. + */ + getQuizById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { + return this.getQuizByField(courseId, 'id', id, forceCache, siteId); + } + + /** + * Get cache key for get quiz access information WS calls. + * + * @param {number} quizId Quiz ID. + * @return {string} Cache key. + */ + protected getQuizAccessInformationCacheKey(quizId: number): string { + return this.ROOT_CACHE_KEY + 'quizAccessInformation:' + quizId; + } + + /** + * Get access information for an attempt. + * + * @param {number} quizId Quiz ID. + * @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache] Whether 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 access information. + */ + getQuizAccessInformation(quizId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + quizid: quizId + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getQuizAccessInformationCacheKey(quizId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_quiz_access_information', params, preSets); + }); + } + + /** + * Get a readable Quiz grade method. + * + * @param {number|string} method Grading method. + * @return {string} Readable grading method. + */ + getQuizGradeMethod(method: number | string): string { + if (typeof method == 'string') { + method = parseInt(method, 10); + } + + switch (method) { + case AddonModQuizProvider.GRADEHIGHEST: + return this.translate.instant('addon.mod_quiz.gradehighest'); + case AddonModQuizProvider.GRADEAVERAGE: + return this.translate.instant('addon.mod_quiz.gradeaverage'); + case AddonModQuizProvider.ATTEMPTFIRST: + return this.translate.instant('addon.mod_quiz.attemptfirst'); + case AddonModQuizProvider.ATTEMPTLAST: + return this.translate.instant('addon.mod_quiz.attemptlast'); + default: + return ''; + } + } + + /** + * Get cache key for get quiz required qtypes WS calls. + * + * @param {number} quizId Quiz ID. + * @return {string} Cache key. + */ + protected getQuizRequiredQtypesCacheKey(quizId: number): string { + return this.ROOT_CACHE_KEY + 'quizRequiredQtypes:' + quizId; + } + + /** + * Get the potential question types that would be required for a given quiz. + * + * @param {number} quizId Quiz ID. + * @param {boolean} [ignoreCache] Whether 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 access information. + */ + getQuizRequiredQtypes(quizId: number, ignoreCache?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + quizid: quizId + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getQuizRequiredQtypesCacheKey(quizId) + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_quiz_required_qtypes', params, preSets).then((response) => { + if (response && response.questiontypes) { + return response.questiontypes; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Given an attempt's layout, return the list of pages. + * + * @param {string} layout Attempt's layout. + * @return {number[]} Pages. + * @description + * An attempt's layout is a string with the question numbers separated by commas. A 0 indicates a change of page. + * Example: 1,2,3,0,4,5,6,0 + * In the example above, first page has questions 1, 2 and 3. Second page has questions 4, 5 and 6. + * + * This function returns a list of pages. + */ + getPagesFromLayout(layout: string): number[] { + const split = layout.split(','), + pages: number[] = []; + let page = 0; + + for (let i = 0; i < split.length; i++) { + if (split[i] == '0') { + pages.push(page); + page++; + } + } + + return pages; + } + + /** + * Given an attempt's layout and a list of questions identified by question slot, + * return the list of pages that have at least 1 of the questions. + * + * @param {string} layout Attempt's layout. + * @param {any} questions List of questions. It needs to be an object where the keys are question slot. + * @return {number[]} Pages. + * @description + * An attempt's layout is a string with the question numbers separated by commas. A 0 indicates a change of page. + * Example: 1,2,3,0,4,5,6,0 + * In the example above, first page has questions 1, 2 and 3. Second page has questions 4, 5 and 6. + * + * This function returns a list of pages. + */ + getPagesFromLayoutAndQuestions(layout: string, questions: any): number[] { + const split = layout.split(','), + pages: number[] = []; + let page = 0, + pageAdded = false; + + for (let i = 0; i < split.length; i++) { + const value = Number(split[i]); + + if (value == 0) { + page++; + pageAdded = false; + } else if (!pageAdded && questions[value]) { + pages.push(page); + pageAdded = true; + } + } + + return pages; + } + + /** + * Given a list of question types, returns the types that aren't supported. + * + * @param {string[]} questionTypes Question types to check. + * @return {string[]} Not supported question types. + */ + getUnsupportedQuestions(questionTypes: string[]): string[] { + const notSupported = []; + + questionTypes.forEach((type) => { + if (type != 'random' && !this.questionDelegate.isQuestionSupported(type)) { + notSupported.push(type); + } + }); + + return notSupported; + } + + /** + * Given a list of access rules names, returns the rules that aren't supported. + * + * @param {string[]} rulesNames Rules to check. + * @return {string[]} Not supported rules names. + */ + getUnsupportedRules(rulesNames: string[]): string[] { + const notSupported = []; + + rulesNames.forEach((name) => { + if (!this.accessRulesDelegate.isAccessRuleSupported(name)) { + notSupported.push(name); + } + }); + + return notSupported; + } + + /** + * Get cache key for get user attempts WS calls. + * + * @param {number} quizId Quiz ID. + * @param {number} userId User ID. + * @return {string} Cache key. + */ + protected getUserAttemptsCacheKey(quizId: number, userId: number): string { + return this.getUserAttemptsCommonCacheKey(quizId) + ':' + userId; + } + + /** + * Get common cache key for get user attempts WS calls. + * + * @param {number} quizId Quiz ID. + * @return {string} Cache key. + */ + protected getUserAttemptsCommonCacheKey(quizId: number): string { + return this.ROOT_CACHE_KEY + 'userAttempts:' + quizId; + } + + /** + * Get quiz attempts for a certain user. + * + * @param {number} quizId Quiz ID. + * @param {number} [status=all] Status of the attempts to get. By default, 'all'. + * @param {boolean} [includePreviews=true] Whether to include previews. Defaults to true. + * @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache] Whether 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 {number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved with the attempts. + */ + getUserAttempts(quizId: number, status: string = 'all', includePreviews: boolean = true, offline?: boolean, + ignoreCache?: boolean, siteId?: string, userId?: number): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const params = { + quizid: quizId, + userid: userId, + status: status, + includepreviews: includePreviews ? 1 : 0 + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserAttemptsCacheKey(quizId, userId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_user_attempts', params, preSets).then((response) => { + if (response && response.attempts) { + return response.attempts; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get cache key for get user best grade WS calls. + * + * @param {number} quizId Quiz ID. + * @param {number} userId User ID. + * @return {string} Cache key. + */ + protected getUserBestGradeCacheKey(quizId: number, userId: number): string { + return this.getUserBestGradeCommonCacheKey(quizId) + ':' + userId; + } + + /** + * Get common cache key for get user best grade WS calls. + * + * @param {number} quizId Quiz ID. + * @return {string} Cache key. + */ + protected getUserBestGradeCommonCacheKey(quizId: number): string { + return this.ROOT_CACHE_KEY + 'userBestGrade:' + quizId; + } + + /** + * Get best grade in a quiz for a certain user. + * + * @param {number} quizId Quiz ID. + * @param {boolean} [ignoreCache] Whether 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 {number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved with the best grade data. + */ + getUserBestGrade(quizId: number, ignoreCache?: boolean, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const params = { + quizid: quizId, + userid: userId + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserBestGradeCacheKey(quizId, userId) + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_quiz_get_user_best_grade', params, preSets); + }); + } + + /** + * Invalidates all the data related to a certain quiz. + * + * @param {number} quizId Quiz ID. + * @param {number} [courseId] Course ID. + * @param {number} [attemptId] Attempt ID to invalidate some WS calls. + * @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 data is invalidated. + */ + invalidateAllQuizData(quizId: number, courseId?: number, attemptId?: number, siteId?: string, userId?: number): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + + promises.push(this.invalidateAttemptAccessInformation(quizId, siteId)); + promises.push(this.invalidateCombinedReviewOptionsForUser(quizId, siteId, userId)); + promises.push(this.invalidateFeedback(quizId, siteId)); + promises.push(this.invalidateQuizAccessInformation(quizId, siteId)); + promises.push(this.invalidateQuizRequiredQtypes(quizId, siteId)); + promises.push(this.invalidateUserAttemptsForUser(quizId, siteId, userId)); + promises.push(this.invalidateUserBestGradeForUser(quizId, siteId, userId)); + + if (attemptId) { + promises.push(this.invalidateAttemptData(attemptId, siteId)); + promises.push(this.invalidateAttemptReview(attemptId, siteId)); + promises.push(this.invalidateAttemptSummary(attemptId, siteId)); + } + + if (courseId) { + promises.push(this.invalidateGradeFromGradebook(courseId, siteId, userId)); + } + + return Promise.all(promises); + } + + /** + * Invalidates attempt access information for all attempts in a quiz. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAttemptAccessInformation(quizId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getAttemptAccessInformationCommonCacheKey(quizId)); + }); + } + + /** + * Invalidates attempt access information for an attempt. + * + * @param {number} quizId Quiz ID. + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAttemptAccessInformationForAttempt(quizId: number, attemptId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAttemptAccessInformationCacheKey(quizId, attemptId)); + }); + } + + /** + * Invalidates attempt data for all pages. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAttemptData(attemptId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getAttemptDataCommonCacheKey(attemptId)); + }); + } + + /** + * Invalidates attempt data for a certain page. + * + * @param {number} attemptId Attempt ID. + * @param {number} page Page. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAttemptDataForPage(attemptId: number, page: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAttemptDataCacheKey(attemptId, page)); + }); + } + + /** + * Invalidates attempt review for all pages. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAttemptReview(attemptId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getAttemptReviewCommonCacheKey(attemptId)); + }); + } + + /** + * Invalidates attempt review for a certain page. + * + * @param {number} attemptId Attempt ID. + * @param {number} page Page. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAttemptReviewForPage(attemptId: number, page: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAttemptReviewCacheKey(attemptId, page)); + }); + } + + /** + * Invalidates attempt summary. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAttemptSummary(attemptId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAttemptSummaryCacheKey(attemptId)); + }); + } + + /** + * Invalidates combined review options for all users. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateCombinedReviewOptions(quizId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getCombinedReviewOptionsCommonCacheKey(quizId)); + }); + } + + /** + * Invalidates combined review options for a certain user. + * + * @param {number} quizId Quiz ID. + * @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 data is invalidated. + */ + invalidateCombinedReviewOptionsForUser(quizId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getCombinedReviewOptionsCacheKey(quizId, userId)); + }); + } + + /** + * Invalidate the prefetched content except files. + * To invalidate files, use AddonModQuizProvider.invalidateFiles. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Get required data to call the invalidate functions. + return this.getQuiz(courseId, moduleId, false, siteId).then((quiz) => { + return this.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => { + // Now invalidate it. + const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined; + + return this.invalidateAllQuizData(quiz.id, courseId, lastAttemptId, siteId); + }); + }); + } + + /** + * Invalidates feedback for all grades of a quiz. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateFeedback(quizId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getFeedbackForGradeCommonCacheKey(quizId)); + }); + } + + /** + * Invalidates feedback for a certain grade. + * + * @param {number} quizId Quiz ID. + * @param {number} grade Grade. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateFeedbackForGrade(quizId: number, grade: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getFeedbackForGradeCacheKey(quizId, grade)); + }); + } + + /** + * Invalidate the prefetched files. + * + * @param {number} moduleId The module ID. + * @return {Promise} Promise resolved when the files are invalidated. + */ + invalidateFiles(moduleId: number): Promise { + return this.filepoolProvider.invalidateFilesByComponent(this.sitesProvider.getCurrentSiteId(), + AddonModQuizProvider.COMPONENT, moduleId); + } + + /** + * Invalidates grade from gradebook for a certain user. + * + * @param {number} courseId Course ID. + * @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 data is invalidated. + */ + invalidateGradeFromGradebook(courseId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return this.gradesHelper.invalidateGradeModuleItems(courseId, userId, null, siteId); + }); + } + + /** + * Invalidates quiz access information for a quiz. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateQuizAccessInformation(quizId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getQuizAccessInformationCacheKey(quizId)); + }); + } + + /** + * Invalidates required qtypes for a quiz. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateQuizRequiredQtypes(quizId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getQuizRequiredQtypesCacheKey(quizId)); + }); + } + + /** + * Invalidates user attempts for all users. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateUserAttempts(quizId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getUserAttemptsCommonCacheKey(quizId)); + }); + } + + /** + * Invalidates user attempts for a certain user. + * + * @param {number} quizId Quiz ID. + * @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 data is invalidated. + */ + invalidateUserAttemptsForUser(quizId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getUserAttemptsCacheKey(quizId, userId)); + }); + } + + /** + * Invalidates user best grade for all users. + * + * @param {number} quizId Quiz ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateUserBestGrade(quizId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getUserBestGradeCommonCacheKey(quizId)); + }); + } + + /** + * Invalidates user best grade for a certain user. + * + * @param {number} quizId Quiz ID. + * @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 data is invalidated. + */ + invalidateUserBestGradeForUser(quizId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getUserBestGradeCacheKey(quizId, userId)); + }); + } + + /** + * Invalidates quiz data. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateQuizData(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getQuizDataCacheKey(courseId)); + }); + } + + /** + * Check if an attempt is finished based on its state. + * + * @param {string} state Attempt's state. + * @return {boolean} Whether it's finished. + */ + isAttemptFinished(state: string): boolean { + return state == AddonModQuizProvider.ATTEMPT_FINISHED || state == AddonModQuizProvider.ATTEMPT_ABANDONED; + } + + /** + * Check if an attempt is finished in offline but not synced. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if finished in offline but not synced, false otherwise. + */ + isAttemptFinishedOffline(attemptId: number, siteId?: string): Promise { + return this.quizOfflineProvider.getAttemptById(attemptId, siteId).then((attempt) => { + return !!attempt.finished; + }).catch(() => { + return false; + }); + } + + /** + * Check if an attempt is nearly over. We consider an attempt nearly over or over if: + * - Is not in progress + * OR + * - It finished before autosaveperiod passes. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @return {boolean} Whether it's nearly over or over. + */ + isAttemptTimeNearlyOver(quiz: any, attempt: any): boolean { + if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { + // Attempt not in progress, return true. + return true; + } + + const dueDate = this.getAttemptDueDate(quiz, attempt), + autoSavePeriod = quiz.autosaveperiod || 0; + + if (dueDate > 0 && Date.now() + autoSavePeriod >= dueDate) { + return true; + } + + return false; + } + + /** + * Check if last attempt is offline and unfinished. + * + * @param {number} attemptId Attempt ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User ID. If not defined, user current site's user. + * @return {Promise} Promise resolved with boolean: true if last offline attempt is unfinished, false otherwise. + */ + isLastAttemptOfflineUnfinished(quiz: any, siteId?: string, userId?: number): Promise { + return this.quizOfflineProvider.getQuizAttempts(quiz.id, siteId, userId).then((attempts) => { + const last = attempts.pop(); + + return last && !last.finished; + }).catch(() => { + return false; + }); + } + + /** + * Check if a quiz navigation is sequential. + * + * @param {any} quiz Quiz. + * @return {boolean} Whether navigation is sequential. + */ + isNavigationSequential(quiz: any): boolean { + return quiz.navmethod == 'sequential'; + } + + /** + * Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the quiz WS are available. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean} Whether the plugin is enabled. + */ + isPluginEnabled(siteId?: string): boolean { + // Quiz WebServices were introduced in 3.1, it will always be enabled. + return true; + } + + /** + * Check if a question is blocked. + * + * @param {any} question Question. + * @return {boolean} Whether it's blocked. + */ + isQuestionBlocked(question: any): boolean { + this.div.innerHTML = question.html; + + return !!this.div.querySelector('.mod_quiz-blocked_question_warning'); + } + + /** + * Check if a quiz is enabled to be used in offline. + * + * @param {any} quiz Quiz. + * @return {boolean} Whether offline is enabled. + */ + isQuizOffline(quiz: any): boolean { + return !!quiz.allowofflineattempts; + } + + /** + * Given a list of attempts, add finishedOffline=true to those attempts that are finished in offline but not synced. + * + * @param {any[]} attempts List of attempts. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + loadFinishedOfflineData(attempts: any[], siteId?: string): Promise { + if (attempts.length) { + // We only need to check the last attempt because the user can only have 1 local attempt. + const lastAttempt = attempts[attempts.length - 1]; + + return this.isAttemptFinishedOffline(lastAttempt.id, siteId).then((finished) => { + lastAttempt.finishedOffline = finished; + }); + } + + return Promise.resolve(); + } + + /** + * Report an attempt as being viewed. + * + * @param {number} attemptId Attempt ID. + * @param {number} [page=0] Page number. + * @param {any} [preflightData] Preflight required data (like password). + * @param {boolean} [offline] Whether attempt is offline. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logViewAttempt(attemptId: number, page: number = 0, preflightData: any = {}, offline?: boolean): Promise { + const params = { + attemptid: attemptId, + page: page, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true) + }, + promises = []; + + promises.push(this.sitesProvider.getCurrentSite().write('mod_quiz_view_attempt', params)); + if (offline) { + promises.push(this.quizOfflineProvider.setAttemptCurrentPage(attemptId, page)); + } + + return Promise.all(promises); + } + + /** + * Report an attempt's review as being viewed. + * + * @param {number} attemptId Attempt ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logViewAttemptReview(attemptId: number): Promise { + const params = { + attemptid: attemptId + }; + + return this.sitesProvider.getCurrentSite().write('mod_quiz_view_attempt_review', params); + } + + /** + * Report an attempt's summary as being viewed. + * + * @param {number} attemptId Attempt ID. + * @param {any} preflightData Preflight required data (like password). + * @return {Promise} Promise resolved when the WS call is successful. + */ + logViewAttemptSummary(attemptId: number, preflightData: any): Promise { + const params = { + attemptid: attemptId, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true) + }; + + return this.sitesProvider.getCurrentSite().write('mod_quiz_view_attempt_summary', params); + } + + /** + * Report a quiz as being viewed. + * + * @param {number} id Module ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logViewQuiz(id: number): Promise { + const params = { + quizid: id + }; + + return this.sitesProvider.getCurrentSite().write('mod_quiz_view_quiz', params); + } + + /** + * Process an attempt, saving its data. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} data Data to save. + * @param {any} preflightData Preflight required data (like password). + * @param {boolean} [finish] Whether to finish the quiz. + * @param {boolean} [timeUp] Whether the quiz time is up, false otherwise. + * @param {boolean} [offline] Whether the attempt is offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + processAttempt(quiz: any, attempt: any, data: any, preflightData: any, finish?: boolean, timeUp?: boolean, offline?: boolean, + siteId?: string): Promise { + if (offline) { + return this.processAttemptOffline(quiz, attempt, data, preflightData, finish, siteId); + } + + return this.processAttemptOnline(attempt.id, data, preflightData, finish, timeUp, siteId); + } + + /** + * Process an online attempt, saving its data. + * + * @param {number} attemptId Attempt ID. + * @param {any} data Data to save. + * @param {any} preflightData Preflight required data (like password). + * @param {boolean} [finish] Whether to finish the quiz. + * @param {boolean} [timeUp] Whether the quiz time is up, false otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + protected processAttemptOnline(attemptId: number, data: any, preflightData: any, finish?: boolean, timeUp?: boolean, + siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + attemptid: attemptId, + data: this.utils.objectToArrayOfObjects(data, 'name', 'value'), + finishattempt: finish ? 1 : 0, + timeup: timeUp ? 1 : 0, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value') + }; + + return site.write('mod_quiz_process_attempt', params).then((response) => { + if (response && response.warnings && response.warnings.length) { + // Reject with the first warning. + return Promise.reject(response.warnings[0]); + } else if (response && response.state) { + return response.state; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Process an offline attempt, saving its data. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} data Data to save. + * @param {any} preflightData Preflight required data (like password). + * @param {boolean} [finish] Whether to finish the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + protected processAttemptOffline(quiz: any, attempt: any, data: any, preflightData: any, finish?: boolean, siteId?: string) + : Promise { + + // Get attempt summary to have the list of questions. + return this.getAttemptSummary(attempt.id, preflightData, true, false, true, siteId).then((questionArray) => { + // Convert the question array to an object. + const questions = this.utils.arrayToObject(questionArray, 'slot'); + + return this.quizOfflineProvider.processAttempt(quiz, attempt, questions, data, finish, siteId); + }); + } + + /** + * Check if it's a graded quiz. Based on Moodle's quiz_has_grades. + * + * @param {any} quiz Quiz. + * @return {boolean} Whether quiz is graded. + */ + quizHasGrades(quiz: any): boolean { + return quiz.grade >= 0.000005 && quiz.sumgrades >= 0.000005; + } + + /** + * Convert the raw grade into a grade out of the maximum grade for this quiz. + * Based on Moodle's quiz_rescale_grade. + * + * @param {string} rawGrade The unadjusted grade, for example attempt.sumgrades. + * @param {any} quiz Quiz. + * @param {boolean|string} format True to format the results for display, 'question' to format a question grade + * (different number of decimal places), false to not format it. + * @return {string} Grade to display. + */ + rescaleGrade(rawGrade: string, quiz: any, format: boolean | string = true): string { + let grade: number; + + const rawGradeNum = parseFloat(rawGrade); + if (!isNaN(rawGradeNum)) { + if (quiz.sumgrades >= 0.000005) { + grade = rawGradeNum * quiz.grade / quiz.sumgrades; + } else { + grade = 0; + } + } + + if (format === 'question') { + return this.formatGrade(grade, this.getGradeDecimals(quiz)); + } else if (format) { + return this.formatGrade(grade, quiz.decimalpoints); + } + + if (grade === null) { + return null; + } else if (typeof grade == 'undefined') { + return undefined; + } + + return String(grade); + } + + /** + * Save an attempt data. + * + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} data Data to save. + * @param {any} preflightData Preflight required data (like password). + * @param {boolean} [offline] Whether attempt is offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + saveAttempt(quiz: any, attempt: any, data: any, preflightData: any, offline?: boolean, siteId?: string): Promise { + try { + if (offline) { + return this.processAttemptOffline(quiz, attempt, data, preflightData, false, siteId); + } + + return this.saveAttemptOnline(attempt.id, data, preflightData, siteId); + } catch (ex) { + this.logger.error(ex); + + return Promise.reject(null); + } + } + + /** + * Save an attempt data. + * + * @param {number} attemptId Attempt ID. + * @param {any} data Data to save. + * @param {any} preflightData Preflight required data (like password). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + protected saveAttemptOnline(attemptId: number, data: any, preflightData: any, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + attemptid: attemptId, + data: this.utils.objectToArrayOfObjects(data, 'name', 'value'), + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value') + }; + + return site.write('mod_quiz_save_attempt', params).then((response) => { + if (response && response.warnings && response.warnings.length) { + // Reject with the first warning. + return Promise.reject(response.warnings[0]); + } else if (!response || !response.status) { + return Promise.reject(null); + } + }); + }); + } + + /** + * Check if time left should be shown. + * + * @param {string[]} rules List of active rules names. + * @param {any} attempt Attempt. + * @param {number} endTime The attempt end time (in seconds). + * @return {boolean} Whether time left should be displayed. + */ + shouldShowTimeLeft(rules: string[], attempt: any, endTime: number): boolean { + const timeNow = this.timeUtils.timestamp(); + + if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { + return false; + } + + return this.accessRulesDelegate.shouldShowTimeLeft(rules, attempt, endTime, timeNow); + } + + /** + * Start an attempt. + * + * @param {number} quizId Quiz ID. + * @param {any} preflightData Preflight required data (like password). + * @param {boolean} [forceNew] Whether to force a new attempt or not. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the attempt data. + */ + startAttempt(quizId: number, preflightData: any, forceNew?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + quizid: quizId, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true), + forcenew: forceNew ? 1 : 0 + }; + + return site.write('mod_quiz_start_attempt', params).then((response) => { + if (response && response.warnings && response.warnings.length) { + // Reject with the first warning. + return Promise.reject(response.warnings[0]); + } else if (response && response.attempt) { + return response.attempt; + } + + return Promise.reject(null); + }); + }); + } +} diff --git a/src/addon/mod/quiz/providers/review-link-handler.ts b/src/addon/mod/quiz/providers/review-link-handler.ts new file mode 100644 index 000000000..2aa6dece7 --- /dev/null +++ b/src/addon/mod/quiz/providers/review-link-handler.ts @@ -0,0 +1,117 @@ +// (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 { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModQuizProvider } from './quiz'; + +/** + * Handler to treat links to quiz review. + */ +@Injectable() +export class AddonModQuizReviewLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModQuizReviewLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModQuiz'; + pattern = /\/mod\/quiz\/review\.php.*([\&\?]attempt=\d+)/; + + constructor(protected domUtils: CoreDomUtilsProvider, protected quizProvider: AddonModQuizProvider, + protected courseHelper: CoreCourseHelperProvider, protected linkHelper: CoreContentLinksHelperProvider) { + 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 { + + courseId = courseId || params.courseid || params.cid; + + return [{ + action: (siteId, navCtrl?): void => { + // Retrieve the quiz ID using the attempt ID. + const modal = this.domUtils.showModalLoading(), + attemptId = parseInt(params.attempt, 10), + page = parseInt(params.page, 10); + let quizId; + + this.getQuizIdByAttemptId(attemptId).then((id) => { + quizId = id; + + // Get the courseId if we don't have it. + if (courseId) { + return courseId; + } else { + return this.courseHelper.getModuleCourseIdByInstance(quizId, 'quiz', siteId); + } + }).then((courseId) => { + // Go to the review page. + const pageParams = { + quizId: quizId, + attemptId: attemptId, + courseId: courseId, + page: params.showall ? -1 : (isNaN(page) ? -1 : page) + }; + + this.linkHelper.goInSite(navCtrl, 'AddonModQuizReviewPage', pageParams, siteId); + }).catch((error) => { + + this.domUtils.showErrorModalDefault(error, 'An error occurred while loading the required data.'); + }).finally(() => { + modal.dismiss(); + }); + } + }]; + } + /** + * Get a quiz ID by attempt ID. + * + * @param {number} attemptId Attempt ID. + * @return {Promise} Promise resolved with the quiz ID. + */ + protected getQuizIdByAttemptId(attemptId: number): Promise { + // Use getAttemptReview to retrieve the quiz ID. + return this.quizProvider.getAttemptReview(attemptId).then((reviewData) => { + if (reviewData.attempt && reviewData.attempt.quiz) { + return reviewData.attempt.quiz; + } + + return Promise.reject(null); + }); + } + + /** + * 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.quizProvider.isPluginEnabled(); + } +} diff --git a/src/addon/mod/quiz/providers/sync-cron-handler.ts b/src/addon/mod/quiz/providers/sync-cron-handler.ts new file mode 100644 index 000000000..d25c2d30b --- /dev/null +++ b/src/addon/mod/quiz/providers/sync-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 { AddonModQuizSyncProvider } from './quiz-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonModQuizSyncCronHandler implements CoreCronHandler { + name = 'AddonModQuizSyncCronHandler'; + + constructor(private quizSync: AddonModQuizSyncProvider) {} + + /** + * 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.quizSync.syncAllQuizzes(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return AddonModQuizSyncProvider.SYNC_TIME; + } +} diff --git a/src/addon/mod/quiz/quiz.module.ts b/src/addon/mod/quiz/quiz.module.ts new file mode 100644 index 000000000..4c0126ed6 --- /dev/null +++ b/src/addon/mod/quiz/quiz.module.ts @@ -0,0 +1,87 @@ +// (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 { CoreCronDelegate } from '@providers/cron'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { AddonModQuizAccessRuleDelegate } from './providers/access-rules-delegate'; +import { AddonModQuizProvider } from './providers/quiz'; +import { AddonModQuizOfflineProvider } from './providers/quiz-offline'; +import { AddonModQuizHelperProvider } from './providers/helper'; +import { AddonModQuizSyncProvider } from './providers/quiz-sync'; +import { AddonModQuizModuleHandler } from './providers/module-handler'; +import { AddonModQuizPrefetchHandler } from './providers/prefetch-handler'; +import { AddonModQuizSyncCronHandler } from './providers/sync-cron-handler'; +import { AddonModQuizIndexLinkHandler } from './providers/index-link-handler'; +import { AddonModQuizGradeLinkHandler } from './providers/grade-link-handler'; +import { AddonModQuizReviewLinkHandler } from './providers/review-link-handler'; +import { AddonModQuizComponentsModule } from './components/components.module'; + +// Access rules. +import { AddonModQuizAccessDelayBetweenAttemptsModule } from './accessrules/delaybetweenattempts/delaybetweenattempts.module'; +import { AddonModQuizAccessIpAddressModule } from './accessrules/ipaddress/ipaddress.module'; +import { AddonModQuizAccessNumAttemptsModule } from './accessrules/numattempts/numattempts.module'; +import { AddonModQuizAccessOfflineAttemptsModule } from './accessrules/offlineattempts/offlineattempts.module'; +import { AddonModQuizAccessOpenCloseDateModule } from './accessrules/openclosedate/openclosedate.module'; +import { AddonModQuizAccessPasswordModule } from './accessrules/password/password.module'; +import { AddonModQuizAccessSafeBrowserModule } from './accessrules/safebrowser/safebrowser.module'; +import { AddonModQuizAccessSecureWindowModule } from './accessrules/securewindow/securewindow.module'; +import { AddonModQuizAccessTimeLimitModule } from './accessrules/timelimit/timelimit.module'; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonModQuizComponentsModule, + AddonModQuizAccessDelayBetweenAttemptsModule, + AddonModQuizAccessIpAddressModule, + AddonModQuizAccessNumAttemptsModule, + AddonModQuizAccessOfflineAttemptsModule, + AddonModQuizAccessOpenCloseDateModule, + AddonModQuizAccessPasswordModule, + AddonModQuizAccessSafeBrowserModule, + AddonModQuizAccessSecureWindowModule, + AddonModQuizAccessTimeLimitModule + ], + providers: [ + AddonModQuizAccessRuleDelegate, + AddonModQuizProvider, + AddonModQuizOfflineProvider, + AddonModQuizHelperProvider, + AddonModQuizSyncProvider, + AddonModQuizModuleHandler, + AddonModQuizPrefetchHandler, + AddonModQuizSyncCronHandler, + AddonModQuizIndexLinkHandler, + AddonModQuizGradeLinkHandler, + AddonModQuizReviewLinkHandler + ] +}) +export class AddonModQuizModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModQuizModuleHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModQuizPrefetchHandler, + cronDelegate: CoreCronDelegate, syncHandler: AddonModQuizSyncCronHandler, linksDelegate: CoreContentLinksDelegate, + indexHandler: AddonModQuizIndexLinkHandler, gradeHandler: AddonModQuizGradeLinkHandler, + reviewHandler: AddonModQuizReviewLinkHandler) { + + moduleDelegate.registerHandler(moduleHandler); + prefetchDelegate.registerHandler(prefetchHandler); + cronDelegate.register(syncHandler); + linksDelegate.registerHandler(indexHandler); + linksDelegate.registerHandler(gradeHandler); + linksDelegate.registerHandler(reviewHandler); + } +} diff --git a/src/addon/mod/resource/providers/module-handler.ts b/src/addon/mod/resource/providers/module-handler.ts index a531d8029..cfb5066a1 100644 --- a/src/addon/mod/resource/providers/module-handler.ts +++ b/src/addon/mod/resource/providers/module-handler.ts @@ -20,6 +20,8 @@ import { AddonModResourceIndexComponent } from '../components/index/index'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreConstants } from '@core/constants'; /** * Handler to support resource modules. @@ -29,8 +31,12 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler { name = 'AddonModResource'; modName = 'resource'; + protected statusObserver; + constructor(protected resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider, - protected mimetypeUtils: CoreMimetypeUtilsProvider, private resourceHelper: AddonModResourceHelperProvider) { } + protected mimetypeUtils: CoreMimetypeUtilsProvider, private resourceHelper: AddonModResourceHelperProvider, + protected prefetchDelegate: CoreCourseModulePrefetchDelegate) { + } /** * Check if the handler is enabled on a site level. @@ -50,6 +56,11 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler { * @return {CoreCourseModuleHandlerData} Data to render the module. */ getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + const updateStatus = (status: string): void => { + handlerData.buttons[0].hidden = status !== CoreConstants.DOWNLOADED || + this.resourceHelper.isDisplayedInIframe(module); + }; + const handlerData = { icon: this.courseProvider.getModuleIconSrc('resource'), title: module.name, @@ -58,8 +69,9 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler { action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { navCtrl.push('AddonModResourceIndexPage', {module: module, courseId: courseId}, options); }, + updateStatus: updateStatus.bind(this), buttons: [ { - hidden: !this.resourceHelper.isDisplayedInIframe(module), + hidden: true, icon: 'document', label: 'addon.mod_resource.openthefile', action: (event: Event, navCtrl: NavController, module: any, courseId: number): void => { @@ -92,7 +104,9 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler { */ protected hideOpenButton(module: any, courseId: number): Promise { return this.courseProvider.loadModuleContents(module, courseId).then(() => { - return this.resourceHelper.isDisplayedInIframe(module); + return this.prefetchDelegate.getModuleStatus(module, courseId).then((status) => { + return status !== CoreConstants.DOWNLOADED || this.resourceHelper.isDisplayedInIframe(module); + }); }); } diff --git a/src/addon/mod/survey/components/index/index.scss b/src/addon/mod/survey/components/index/index.scss index d9cbd7ee7..1b50cccd2 100644 --- a/src/addon/mod/survey/components/index/index.scss +++ b/src/addon/mod/survey/components/index/index.scss @@ -13,15 +13,6 @@ addon-mod-survey-index { background-color: $white; } - ion-select { - float: right; - max-width: none; - .select-text { - white-space: normal; - text-align: right; - } - } - .even { background-color: $gray-light; } diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index a5e8e234b..731ce8b66 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -188,15 +188,15 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo }); } - this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then(() => { + return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then(() => { this.content.scrollToTop(); return this.refreshContent(false); - }).catch((message) => { - this.domUtils.showErrorModalDefault(message, 'addon.mod_survey.cannotsubmitsurvey', true); }).finally(() => { modal.dismiss(); }); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'addon.mod_survey.cannotsubmitsurvey', true); }); } diff --git a/src/addon/mod/survey/providers/sync.ts b/src/addon/mod/survey/providers/sync.ts index 7c3aaa35e..d0717acf8 100644 --- a/src/addon/mod/survey/providers/sync.ts +++ b/src/addon/mod/survey/providers/sync.ts @@ -35,12 +35,14 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { static AUTO_SYNCED = 'addon_mod_survey_autom_synced'; protected componentTranslate: string; - constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, - protected appProvider: CoreAppProvider, private surveyOffline: AddonModSurveyOfflineProvider, + constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, + syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, + courseProvider: CoreCourseProvider, private surveyOffline: AddonModSurveyOfflineProvider, private eventsProvider: CoreEventsProvider, private surveyProvider: AddonModSurveyProvider, - private translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, - courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider) { - super('AddonModSurveySyncProvider', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils); + private utils: CoreUtilsProvider) { + + super('AddonModSurveySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); + this.componentTranslate = courseProvider.translateModuleName('survey'); } diff --git a/src/addon/notes/pages/add/add.html b/src/addon/notes/pages/add/add.html index e0a41c7b1..856fddb6e 100644 --- a/src/addon/notes/pages/add/add.html +++ b/src/addon/notes/pages/add/add.html @@ -12,7 +12,7 @@
{{ 'addon.notes.publishstate' | translate }} - + {{ 'addon.notes.personalnotes' | translate }} {{ 'addon.notes.coursenotes' | translate }} {{ 'addon.notes.sitenotes' | translate }} diff --git a/src/addon/notes/providers/notes-sync.ts b/src/addon/notes/providers/notes-sync.ts index d443385db..059d61530 100644 --- a/src/addon/notes/providers/notes-sync.ts +++ b/src/addon/notes/providers/notes-sync.ts @@ -34,12 +34,13 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { static AUTO_SYNCED = 'addon_notes_autom_synced'; - constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, - protected appProvider: CoreAppProvider, private notesOffline: AddonNotesOfflineProvider, + constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, + syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, + private notesOffline: AddonNotesOfflineProvider, private utils: CoreUtilsProvider, private eventsProvider: CoreEventsProvider, private notesProvider: AddonNotesProvider, - private coursesProvider: CoreCoursesProvider, private translate: TranslateService, private utils: CoreUtilsProvider, - syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider) { - super('AddonNotesSync', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils); + private coursesProvider: CoreCoursesProvider) { + + super('AddonNotesSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); } /** diff --git a/src/addon/qbehaviour/deferredcbm/component/deferredcbm.html b/src/addon/qbehaviour/deferredcbm/component/deferredcbm.html index 07c1ffc7d..0975635c1 100644 --- a/src/addon/qbehaviour/deferredcbm/component/deferredcbm.html +++ b/src/addon/qbehaviour/deferredcbm/component/deferredcbm.html @@ -1,6 +1,16 @@ - -

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

-
- -

-
+
+ +

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

+
+
+ + + + + + +
+ + + +
diff --git a/src/addon/qtype/calculated/component/calculated.html b/src/addon/qtype/calculated/component/calculated.html index af87387cc..4f100766f 100644 --- a/src/addon/qtype/calculated/component/calculated.html +++ b/src/addon/qtype/calculated/component/calculated.html @@ -8,24 +8,26 @@ - - - - - - + + + + + + + - - - - - + + + + + - - - - - + + + + + + @@ -38,18 +40,26 @@ - + {{option.label}} - + + + -
- -

{{option.text}}

-
+
+ + +

{{option.text}}

+
+ +
+ + +
diff --git a/src/addon/qtype/ddimageortext/classes/ddimageortext.ts b/src/addon/qtype/ddimageortext/classes/ddimageortext.ts index 64123bee7..4d77e03f0 100644 --- a/src/addon/qtype/ddimageortext/classes/ddimageortext.ts +++ b/src/addon/qtype/ddimageortext/classes/ddimageortext.ts @@ -45,6 +45,7 @@ export class AddonQtypeDdImageOrTextQuestion { protected topNode: HTMLElement; protected proportion = 1; protected selected: HTMLElement; // Selected element (being "dragged"). + protected resizeFunction; /** * Create the this. @@ -182,7 +183,10 @@ export class AddonQtypeDdImageOrTextQuestion { */ destroy(): void { this.stopPolling(); - window.removeEventListener('resize', this.resizeFunction); + + if (this.resizeFunction) { + window.removeEventListener('resize', this.resizeFunction); + } } /** @@ -192,7 +196,7 @@ export class AddonQtypeDdImageOrTextQuestion { * @return {AddonQtypeDdImageOrTextQuestionDocStructure} The object. */ docStructure(slot: number): AddonQtypeDdImageOrTextQuestionDocStructure { - const topNode = this.container.querySelector(`#core-question-${slot} .addon-qtype-ddimageortext-container`), + const topNode = this.container.querySelector('.addon-qtype-ddimageortext-container'), dragItemsArea = topNode.querySelector('div.dragitems'), doc: AddonQtypeDdImageOrTextQuestionDocStructure = {}; @@ -456,6 +460,7 @@ export class AddonQtypeDdImageOrTextQuestion { this.pollForImageLoad(); }); + this.resizeFunction = this.repositionDragsForQuestion.bind(this); window.addEventListener('resize', this.resizeFunction); } @@ -637,13 +642,6 @@ export class AddonQtypeDdImageOrTextQuestion { } } - /** - * Function to call when the window is resized. - */ - resizeFunction(): void { - this.repositionDragsForQuestion(); - } - /** * Mark a draggable element as selected. * diff --git a/src/addon/qtype/ddimageortext/component/ddimageortext.html b/src/addon/qtype/ddimageortext/component/ddimageortext.html index b6394b880..23d97495f 100644 --- a/src/addon/qtype/ddimageortext/component/ddimageortext.html +++ b/src/addon/qtype/ddimageortext/component/ddimageortext.html @@ -3,11 +3,11 @@ -

- +

+ {{ 'core.question.howtodraganddrop' | translate }}

- +
diff --git a/src/addon/qtype/ddimageortext/component/ddimageortext.scss b/src/addon/qtype/ddimageortext/component/ddimageortext.scss new file mode 100644 index 000000000..48c69d82e --- /dev/null +++ b/src/addon/qtype/ddimageortext/component/ddimageortext.scss @@ -0,0 +1,101 @@ +// Style ddimageortext content a bit. Almost all these styles are copied from Moodle. +addon-qtype-ddimageortext { + .qtext { + margin-bottom: 0.5em; + display: block; + } + + div.droparea img { + border: 1px solid $gray-darker; + max-width: 100%; + } + + .draghome { + vertical-align: top; + margin: 5px; + visibility : hidden; + } + .draghome img { + display: block; + } + + div.draghome { + border: 1px solid $gray-darker; + cursor: pointer; + background-color: #B0C4DE; + display:inline-block; + height: auto; + width: auto; + 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; + } + .drag { + border: 1px solid $gray-darker; + cursor: pointer; + z-index: 2; + } + .dragitems.readonly .drag { + cursor: auto; + } + .dragitems>div { + clear: both; + } + .dragitems { + cursor: pointer; + } + .dragitems.readonly { + cursor: auto; + } + .drag img { + display: block; + } + + div.ddarea { + text-align : center; + position: relative; + } + .dropbackground { + margin:0 auto; + } + .dropzone { + border: 1px solid $gray-darker; + position: absolute; + z-index: 1; + cursor: pointer; + } + .readonly .dropzone { + cursor: auto; + } + + div.dragitems div.draghome, div.dragitems div.drag { + font:13px/1.231 arial,helvetica,clean,sans-serif; + } + .drag.beingdragged { + z-index: 3; + box-shadow: 3px 3px 4px $gray-darker; + } +} diff --git a/src/addon/qtype/ddimageortext/component/ddimageortext.ts b/src/addon/qtype/ddimageortext/component/ddimageortext.ts index cb96133bd..cc927fbc9 100644 --- a/src/addon/qtype/ddimageortext/component/ddimageortext.ts +++ b/src/addon/qtype/ddimageortext/component/ddimageortext.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, AfterViewInit, Injector, ElementRef } from '@angular/core'; +import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext'; @@ -24,14 +24,15 @@ import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext'; selector: 'addon-qtype-ddimageortext', templateUrl: 'ddimageortext.html' }) -export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy { +export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy { protected element: HTMLElement; protected questionInstance: AddonQtypeDdImageOrTextQuestion; protected drops: any[]; // The drop zones received in the init object of the question. + protected destroyed = false; - constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) { - super(logger, 'AddonQtypeDdImageOrTextComponent', injector); + constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) { + super(loggerProvider, 'AddonQtypeDdImageOrTextComponent', injector); this.element = element.nativeElement; } @@ -76,18 +77,21 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent } /** - * View has been initialized. + * The question has been rendered. */ - ngAfterViewInit(): void { - // Create the instance. - this.questionInstance = new AddonQtypeDdImageOrTextQuestion(this.logger, this.domUtils, this.element, - this.question, this.question.readOnly, this.drops); + questionRendered(): void { + if (!this.destroyed) { + // Create the instance. + this.questionInstance = new AddonQtypeDdImageOrTextQuestion(this.loggerProvider, this.domUtils, this.element, + this.question, this.question.readOnly, this.drops); + } } /** * Component being destroyed. */ ngOnDestroy(): void { + this.destroyed = true; this.questionInstance && this.questionInstance.destroy(); } } diff --git a/src/addon/qtype/ddmarker/classes/ddmarker.ts b/src/addon/qtype/ddmarker/classes/ddmarker.ts index 3624bf46a..93a368242 100644 --- a/src/addon/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addon/qtype/ddmarker/classes/ddmarker.ts @@ -50,6 +50,7 @@ export class AddonQtypeDdMarkerQuestion { protected proportion = 1; protected selected: HTMLElement; // Selected element (being "dragged"). protected graphics: AddonQtypeDdMarkerGraphicsApi; + protected resizeFunction; doc: AddonQtypeDdMarkerQuestionDocStructure; shapes = []; @@ -157,7 +158,9 @@ export class AddonQtypeDdMarkerQuestion { * Function to call when the instance is no longer needed. */ destroy(): void { - window.removeEventListener('resize', this.resizeFunction); + if (this.resizeFunction) { + window.removeEventListener('resize', this.resizeFunction); + } } /** @@ -167,7 +170,7 @@ export class AddonQtypeDdMarkerQuestion { * @return {AddonQtypeDdMarkerQuestionDocStructure} The object. */ docStructure(slot: number): AddonQtypeDdMarkerQuestionDocStructure { - const topNode = this.container.querySelector('#core-question-' + slot + ' .addon-qtype-ddmarker-container'), + const topNode = this.container.querySelector('.addon-qtype-ddmarker-container'), dragItemsArea = topNode.querySelector('div.dragitems'); return { @@ -293,9 +296,9 @@ export class AddonQtypeDdMarkerQuestion { 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'); + existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo + ' a'); } else { - existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo); + existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo); } if (existingMarkerText) { @@ -538,7 +541,7 @@ export class AddonQtypeDdMarkerQuestion { dragging = (this.doc.dragItemBeingDragged(choiceNo) !== null), coords: number[][] = []; - if (fv !== '' && typeof fv != 'undefined') { + if (fv !== '' && typeof fv != 'undefined' && fv !== null) { // Get all the coordinates in the input and add them to the coords list. const coordsStrings = fv.split(';'); @@ -645,6 +648,7 @@ export class AddonQtypeDdMarkerQuestion { this.pollForImageLoad(); }); + this.resizeFunction = this.redrawDragsAndDrops.bind(this); window.addEventListener('resize', this.resizeFunction); } @@ -731,6 +735,9 @@ export class AddonQtypeDdMarkerQuestion { }, 500); } + /** + * Redraw all draggables and drop zones. + */ redrawDragsAndDrops(): void { // Mark all the draggable items as not placed. const drags = this.doc.dragItems(); @@ -789,7 +796,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, true); } } } @@ -803,13 +810,6 @@ export class AddonQtypeDdMarkerQuestion { this.setFormValue(choiceNo, ''); } - /** - * Function to call when the window is resized. - */ - resizeFunction(): void { - this.redrawDragsAndDrops(); - } - /** * Restart the colour index. */ diff --git a/src/addon/qtype/ddmarker/component/ddmarker.html b/src/addon/qtype/ddmarker/component/ddmarker.html index eb3a16b81..f6ada3e34 100644 --- a/src/addon/qtype/ddmarker/component/ddmarker.html +++ b/src/addon/qtype/ddmarker/component/ddmarker.html @@ -3,11 +3,11 @@ -

- +

+ {{ 'core.question.howtodraganddrop' | translate }}

- +
diff --git a/src/addon/qtype/ddmarker/component/ddmarker.scss b/src/addon/qtype/ddmarker/component/ddmarker.scss new file mode 100644 index 000000000..7502a91a6 --- /dev/null +++ b/src/addon/qtype/ddmarker/component/ddmarker.scss @@ -0,0 +1,97 @@ +// Style ddmarker content a bit. Almost all these styles are copied from Moodle. +addon-qtype-ddmarker { + .qtext { + margin-bottom: 0.5em; + display: block; + } + + div.droparea img { + border: 1px solid $gray-darker; + max-width: 100%; + } + + .draghome img, .draghome span { + visibility: hidden; + } + + .dragitems .dragitem { + cursor: pointer; + position: absolute; + z-index: 2; + } + + .dropzones { + position: absolute; + } + .dropzones svg { + z-index: 3; + } + + .dragitem.beingdragged .markertext { + z-index: 5; + box-shadow: 3px 3px 4px $gray-darker; + } + .dragitems .draghome { + margin: 10px; + display: inline-block; + } + + .dragitems.readonly .dragitem { + cursor: auto; + } + div.ddarea { + text-align: center; + } + div.ddarea .markertexts { + min-height: 80px; + position: absolute; + text-align: left; + } + .dropbackground { + margin: 0 auto; + } + + div.dragitems div.draghome, div.dragitems div.dragitem, + div.draghome, div.drag { + font: 13px/1.231 arial,helvetica,clean,sans-serif; + } + div.dragitems span.markertext, + div.markertexts span.markertext { + margin: 0 5px; + z-index: 2; + background-color: $white; + border: 2px solid $gray-darker; + padding: 5px; + display: inline-block; + zoom: 1; + border-radius: 10px; + } + div.markertexts span.markertext { + z-index: 3; + background-color: $yellow-light; + border-style: solid; + border-width: 2px; + border-color: $yellow; + position: absolute; + } + span.wrongpart { + background-color: $yellow-light; + border-style: solid; + border-width: 2px; + border-color: $yellow; + padding: 5px; + border-radius: 10px; + filter: alpha(opacity=60); + opacity: 0.6; + margin: 5px; + display: inline-block; + } + div.dragitems img.target { + position: absolute; + left: -7px; /* This must be half the size of the target image, minus 0.5. */ + top: -7px; /* In other words, this works for a 15x15 cross-hair. */ + } + div.dragitems div.draghome img.target { + display: none; + } +} diff --git a/src/addon/qtype/ddmarker/component/ddmarker.ts b/src/addon/qtype/ddmarker/component/ddmarker.ts index 2afbe0a1f..e9b16521a 100644 --- a/src/addon/qtype/ddmarker/component/ddmarker.ts +++ b/src/addon/qtype/ddmarker/component/ddmarker.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, AfterViewInit, Injector, ElementRef } from '@angular/core'; +import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker'; @@ -24,14 +24,15 @@ import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker'; selector: 'addon-qtype-ddmarker', templateUrl: 'ddmarker.html' }) -export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy { +export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy { protected element: HTMLElement; protected questionInstance: AddonQtypeDdMarkerQuestion; protected dropZones: any[]; // The drop zones received in the init object of the question. + protected destroyed = false; - constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) { - super(logger, 'AddonQtypeDdMarkerComponent', injector); + constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) { + super(loggerProvider, 'AddonQtypeDdMarkerComponent', injector); this.element = element.nativeElement; } @@ -83,18 +84,21 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple } /** - * View has been initialized. + * The question has been rendered. */ - ngAfterViewInit(): void { - // Create the instance. - this.questionInstance = new AddonQtypeDdMarkerQuestion(this.logger, this.domUtils, this.textUtils, this.element, - this.question, this.question.readOnly, this.dropZones); + questionRendered(): void { + if (!this.destroyed) { + // Create the instance. + this.questionInstance = new AddonQtypeDdMarkerQuestion(this.loggerProvider, this.domUtils, this.textUtils, this.element, + this.question, this.question.readOnly, this.dropZones); + } } /** * Component being destroyed. */ ngOnDestroy(): void { + this.destroyed = true; this.questionInstance && this.questionInstance.destroy(); } } diff --git a/src/addon/qtype/ddwtos/classes/ddwtos.ts b/src/addon/qtype/ddwtos/classes/ddwtos.ts index 648c907fa..c6fb9c365 100644 --- a/src/addon/qtype/ddwtos/classes/ddwtos.ts +++ b/src/addon/qtype/ddwtos/classes/ddwtos.ts @@ -46,6 +46,7 @@ export class AddonQtypeDdwtosQuestion { protected selectors: AddonQtypeDdwtosQuestionCSSSelectors; // Result of cssSelectors. protected placed: {[no: number]: number}; // Map that relates drag elements numbers with drop zones numbers. protected selected: HTMLElement; // Selected element (being "dragged"). + protected resizeFunction; /** * Create the instance. @@ -125,7 +126,7 @@ export class AddonQtypeDdwtosQuestion { * @return {AddonQtypeDdwtosQuestionCSSSelectors} Object with the functions to get the selectors. */ cssSelectors(slot: number): AddonQtypeDdwtosQuestionCSSSelectors { - const topNode = '#core-question-' + slot + ' .addon-qtype-ddwtos-container', + const topNode = '.addon-qtype-ddwtos-container', selectors: AddonQtypeDdwtosQuestionCSSSelectors = {}; selectors.topNode = (): string => { @@ -193,7 +194,9 @@ export class AddonQtypeDdwtosQuestion { * Function to call when the instance is no longer needed. */ destroy(): void { - window.removeEventListener('resize', this.resizeFunction); + if (this.resizeFunction) { + window.removeEventListener('resize', this.resizeFunction); + } } /** @@ -285,6 +288,7 @@ export class AddonQtypeDdwtosQuestion { this.positionDragItems(); }); + this.resizeFunction = this.positionDragItems.bind(this); window.addEventListener('resize', this.resizeFunction); } @@ -488,13 +492,6 @@ export class AddonQtypeDdwtosQuestion { this.placeDragInDrop(null, drop); } - /** - * Function to call when the window is resized. - */ - resizeFunction(): void { - this.positionDragItems(); - } - /** * Select a certain element as being "dragged". * diff --git a/src/addon/qtype/ddwtos/component/ddwtos.html b/src/addon/qtype/ddwtos/component/ddwtos.html index 689a75406..1152d54f3 100644 --- a/src/addon/qtype/ddwtos/component/ddwtos.html +++ b/src/addon/qtype/ddwtos/component/ddwtos.html @@ -1,11 +1,11 @@ -
- -

- +

+ +

+ {{ 'core.question.howtodraganddrop' | translate }}

- +
diff --git a/src/addon/qtype/ddwtos/component/ddwtos.scss b/src/addon/qtype/ddwtos/component/ddwtos.scss new file mode 100644 index 000000000..69d5ae1c1 --- /dev/null +++ b/src/addon/qtype/ddwtos/component/ddwtos.scss @@ -0,0 +1,108 @@ +// Style ddwtos content a bit. Almost all these styles are copied from Moodle. +addon-qtype-ddwtos { + .qtext { + margin-bottom: 0.5em; + display: block; + } + + .draghome { + margin-bottom: 1em; + } + + .answertext { + margin-bottom: 0.5em; + } + + .drop { + display: inline-block; + text-align: center; + border: 1px solid $gray-darker; + margin-bottom: 2px; + border-radius: 5px; + } + .draghome, .drag { + display: inline-block; + text-align: center; + background: transparent; + border: 0; + } + .draghome, .drag.unplaced{ + border: 1px solid $gray-darker; + border-radius: 5px; + } + .draghome { + visibility: hidden; + } + .drag { + z-index: 2; + border-radius: 5px; + } + .drag.selected { + z-index: 3; + box-shadow: 3px 3px 4px $gray-darker; + } + + .drop.selected { + border-color: $yellow-light; + box-shadow: 0 0 5px 5px $yellow-light; + } + + &.notreadonly .drag, + &.notreadonly .draghome, + &.notreadonly .drop, + &.notreadonly .answercontainer { + cursor: pointer; + border-radius: 5px; + } + + &.readonly .drag, + &.readonly .draghome, + &.readonly .drop, + &.readonly .answercontainer { + cursor: default; + } + + span.incorrect { + background-color: $red-light; + } + span.correct { + 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; + } + + sub, sup { + font-size: 80%; + position: relative; + vertical-align: baseline; + } + sup { + top: -0.4em; + } + sub { + bottom: -0.2em; + } +} diff --git a/src/addon/qtype/ddwtos/component/ddwtos.ts b/src/addon/qtype/ddwtos/component/ddwtos.ts index 18a4a121a..2c5e22b69 100644 --- a/src/addon/qtype/ddwtos/component/ddwtos.ts +++ b/src/addon/qtype/ddwtos/component/ddwtos.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, AfterViewInit, Injector, ElementRef } from '@angular/core'; +import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos'; @@ -24,14 +24,15 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos'; selector: 'addon-qtype-ddwtos', templateUrl: 'ddwtos.html' }) -export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy { +export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy { protected element: HTMLElement; protected questionInstance: AddonQtypeDdwtosQuestion; - protected inputIds: string[]; // Ids of the inputs of the question (where the answers will be stored). + protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored). + protected destroyed = false; - constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) { - super(logger, 'AddonQtypeDdwtosComponent', injector); + constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) { + super(loggerProvider, 'AddonQtypeDdwtosComponent', injector); this.element = element.nativeElement; } @@ -54,7 +55,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme this.questionHelper.replaceFeedbackClasses(div); // Treat the correct/incorrect icons. - this.questionHelper.treatCorrectnessIcons(div, this.component, this.componentId); + this.questionHelper.treatCorrectnessIcons(div); const answerContainer = div.querySelector('.answercontainer'); if (!answerContainer) { @@ -74,28 +75,32 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme } // Get the inputs where the answers will be stored and add them to the question text. - const inputEls = Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])')), - inputIds = []; + const inputEls = Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])')); inputEls.forEach((inputEl) => { this.question.text += inputEl.outerHTML; - inputIds.push(inputEl.getAttribute('id')); + this.inputIds.push(inputEl.getAttribute('id')); }); } /** - * View has been initialized. + * The question has been rendered. */ - ngAfterViewInit(): void { - // Create the instance. - this.questionInstance = new AddonQtypeDdwtosQuestion(this.logger, this.domUtils, this.element, this.question, - this.question.readOnly, this.inputIds); + questionRendered(): void { + if (!this.destroyed) { + // Create the instance. + this.questionInstance = new AddonQtypeDdwtosQuestion(this.loggerProvider, this.domUtils, this.element, this.question, + this.question.readOnly, this.inputIds); + + this.questionHelper.treatCorrectnessIconsClicks(this.element, this.component, this.componentId); + } } /** * Component being destroyed. */ ngOnDestroy(): void { + this.destroyed = true; this.questionInstance && this.questionInstance.destroy(); } } diff --git a/src/addon/qtype/essay/component/essay.html b/src/addon/qtype/essay/component/essay.html index 1098c8304..cb1a94af4 100644 --- a/src/addon/qtype/essay/component/essay.html +++ b/src/addon/qtype/essay/component/essay.html @@ -7,18 +7,18 @@ - + - {{question.textarea.text}} + - + + [component]="component" [componentId]="componentId" --> - +

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

@@ -27,7 +27,7 @@
- +

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

diff --git a/src/addon/qtype/essay/component/essay.ts b/src/addon/qtype/essay/component/essay.ts index c421fa629..ba298ce37 100644 --- a/src/addon/qtype/essay/component/essay.ts +++ b/src/addon/qtype/essay/component/essay.ts @@ -15,6 +15,7 @@ import { Component, OnInit, Injector } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; +import { FormControl, FormBuilder } from '@angular/forms'; /** * Component to render an essay question. @@ -25,7 +26,9 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question- }) export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit { - constructor(logger: CoreLoggerProvider, injector: Injector) { + protected formControl: FormControl; + + constructor(logger: CoreLoggerProvider, injector: Injector, protected fb: FormBuilder) { super(logger, 'AddonQtypeEssayComponent', injector); } @@ -34,5 +37,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen */ ngOnInit(): void { this.initEssayComponent(); + + this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text); } } diff --git a/src/addon/qtype/gapselect/component/gapselect.html b/src/addon/qtype/gapselect/component/gapselect.html index eaeb68661..70efb95b7 100644 --- a/src/addon/qtype/gapselect/component/gapselect.html +++ b/src/addon/qtype/gapselect/component/gapselect.html @@ -1,5 +1,5 @@
-

+

diff --git a/src/addon/qtype/gapselect/component/gapselect.scss b/src/addon/qtype/gapselect/component/gapselect.scss new file mode 100644 index 000000000..b467d9351 --- /dev/null +++ b/src/addon/qtype/gapselect/component/gapselect.scss @@ -0,0 +1,19 @@ +// Style gapselect content a bit. All these styles are copied from Moodle. +addon-qtype-gapselect { + p { + margin: 0 0 .5em; + } + + select { + height: 30px; + line-height: 30px; + display: inline-block; + border: 1px solid $gray-dark; + padding: 4px 6px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + margin-bottom: 10px; + background: $gray-lighter; + } +} diff --git a/src/addon/qtype/gapselect/component/gapselect.ts b/src/addon/qtype/gapselect/component/gapselect.ts index 70a4622cf..9bd21aafd 100644 --- a/src/addon/qtype/gapselect/component/gapselect.ts +++ b/src/addon/qtype/gapselect/component/gapselect.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Injector } from '@angular/core'; +import { Component, OnInit, Injector, ElementRef } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; @@ -25,8 +25,12 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question- }) export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent implements OnInit { - constructor(logger: CoreLoggerProvider, injector: Injector) { + protected element: HTMLElement; + + constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) { super(logger, 'AddonQtypeGapSelectComponent', injector); + + this.element = element.nativeElement; } /** @@ -35,4 +39,11 @@ export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent impl ngOnInit(): void { this.initOriginalTextComponent('.qtext'); } + + /** + * The question has been rendered. + */ + questionRendered(): void { + this.questionHelper.treatCorrectnessIconsClicks(this.element, this.component, this.componentId); + } } diff --git a/src/addon/qtype/match/component/match.html b/src/addon/qtype/match/component/match.html index fb40981c6..cfa627279 100644 --- a/src/addon/qtype/match/component/match.html +++ b/src/addon/qtype/match/component/match.html @@ -3,17 +3,21 @@

- - -

-
- - - - {{option.label}} - - - -
+ + + +

+
+ + + + {{option.label}} + + + + + +
+
diff --git a/src/addon/qtype/multianswer/component/multianswer.html b/src/addon/qtype/multianswer/component/multianswer.html index ae07a5f43..899f1c34e 100644 --- a/src/addon/qtype/multianswer/component/multianswer.html +++ b/src/addon/qtype/multianswer/component/multianswer.html @@ -1,5 +1,5 @@
-

+

diff --git a/src/addon/qtype/multianswer/component/multianswer.scss b/src/addon/qtype/multianswer/component/multianswer.scss new file mode 100644 index 000000000..d449e8a26 --- /dev/null +++ b/src/addon/qtype/multianswer/component/multianswer.scss @@ -0,0 +1,43 @@ +// Style multianswer content a bit. All these styles are copied from Moodle. +addon-qtype-multianswer { + p { + margin: 0 0 .5em; + } + + .answer div.r0, .answer div.r1, .answer td.r0, .answer td.r1 { + padding: 0.3em; + } + + table { + width: 100%; + display: table; + } + + tr { + display: table-row; + } + + td { + display: table-cell; + } + + input, select { + display: inline-block; + border: 1px solid #ccc; + padding: 4px 6px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + margin-bottom: 10px; + } + + select { + height: 30px; + line-height: 30px; + } + + input[type="radio"], input[type="checkbox"] { + margin-top: -4px; + margin-right: 7px; + } +} diff --git a/src/addon/qtype/multianswer/component/multianswer.ts b/src/addon/qtype/multianswer/component/multianswer.ts index 79cdba492..8e1ca2068 100644 --- a/src/addon/qtype/multianswer/component/multianswer.ts +++ b/src/addon/qtype/multianswer/component/multianswer.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Injector } from '@angular/core'; +import { Component, OnInit, Injector, ElementRef } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; @@ -25,8 +25,12 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question- }) export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent implements OnInit { - constructor(logger: CoreLoggerProvider, injector: Injector) { + protected element: HTMLElement; + + constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) { super(logger, 'AddonQtypeMultiAnswerComponent', injector); + + this.element = element.nativeElement; } /** @@ -35,4 +39,11 @@ export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent im ngOnInit(): void { this.initOriginalTextComponent('.formulation'); } + + /** + * The question has been rendered. + */ + questionRendered(): void { + this.questionHelper.treatCorrectnessIconsClicks(this.element, this.component, this.componentId); + } } diff --git a/src/addon/qtype/multichoice/component/multichoice.html b/src/addon/qtype/multichoice/component/multichoice.html index ee734dd99..a7dd5baf2 100644 --- a/src/addon/qtype/multichoice/component/multichoice.html +++ b/src/addon/qtype/multichoice/component/multichoice.html @@ -7,19 +7,29 @@ - -

-

-
+ + + +

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

- +
+
+ + +
diff --git a/src/addon/qtype/shortanswer/component/shortanswer.html b/src/addon/qtype/shortanswer/component/shortanswer.html index 104534d90..bbf7f47bf 100644 --- a/src/addon/qtype/shortanswer/component/shortanswer.html +++ b/src/addon/qtype/shortanswer/component/shortanswer.html @@ -2,6 +2,6 @@

- + diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a1d70141e..148627057 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -81,6 +81,7 @@ import { AddonModLabelModule } from '@addon/mod/label/label.module'; import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; import { AddonModFolderModule } from '@addon/mod/folder/folder.module'; import { AddonModPageModule } from '@addon/mod/page/page.module'; +import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module'; import { AddonModUrlModule } from '@addon/mod/url/url.module'; import { AddonModSurveyModule } from '@addon/mod/survey/survey.module'; import { AddonMessageOutputModule } from '@addon/messageoutput/messageoutput.module'; @@ -174,6 +175,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModResourceModule, AddonModFolderModule, AddonModPageModule, + AddonModQuizModule, AddonModUrlModule, AddonModSurveyModule, AddonMessageOutputModule, diff --git a/src/app/app.scss b/src/app/app.scss index c42c14c87..31ac3d32f 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -32,11 +32,12 @@ .img-responsive { display: block; max-width: 100%; - height: auto; + /* height: auto; */ } .opacity-hide { opacity: 0; } .core-big { font-size: 115%; } +.invisible { visibility: hidden; } @include media-breakpoint-up(sm) { .core-center-view .scroll-content { @@ -76,6 +77,10 @@ opacity: 0.6; } +.item-radio-checked { + background-color: $gray-lighter; +} + .core-oauth-icon, .item.core-oauth-icon, .list .item.core-oauth-icon { min-height: 32px; img, .label { @@ -336,6 +341,16 @@ ion-select { position: relative } +ion-col ion-select { + float: right; + max-width: none; + width: 100%; + .select-text { + white-space: normal; + text-align: right; + } + } + // File uploader. // ------------------------- @@ -355,6 +370,85 @@ ion-select { } } +// Question. +// ------------------------- + +.core-question-answer-correct, +.core-question-comment { + color: $core-question-correct-color; + background-color: $core-question-correct-color-bg; + + .label, ion-label.label { + color: $core-question-correct-color; + } +} + +.core-question-answer-incorrect, +.core-question-incorrect { + color: $core-question-incorrect-color; + background-color: $core-question-incorrect-color-bg; + + .label, ion-label.label { + color: $core-question-incorrect-color; + } +} + +.core-question-feedback-container { + background-color: $core-question-feedback-color-bg; + color: $core-question-feedback-color; + + .specificfeedback, .rightanswer, .im-feedback, .feedback, .generalfeedback { + margin: 0 0 .5em; + } + + .correctness { + display: inline-block; + padding: 2px 4px; + font-weight: bold; + line-height: 14px; + color: $white; + text-shadow: 0 -1px 0 rgba(0,0,0,0.25); + background-color: $gray-dark; + -webkit-border-radius: 3px; + border-radius: 3px; + + &.incorrect { + background-color: $red; + } + &.correct { + background-color: $green; + } + } +} + +.core-question-feedback-inline { + display: inline-block; +} + +.core-question-feedback-padding { + padding: 8px 35px 8px 14px; +} + +.core-question-correct { + background-color: $core-question-state-correct-color; +} +.core-question-partiallycorrect { + background-color: $core-question-state-partial-color; +} +.core-question-notanswered, +.core-question-incorrect { + background-color: $core-question-state-incorrect-color; +} + +.core-question-warning { + color: $core-question-warning-color; +} + +.questioncorrectnessicon, +.fa.icon.questioncorrectnessicon { + font-size: 20px; +} + // Atto styles // ------------------------- .atto_image_preview { @@ -509,4 +603,27 @@ textarea { @extend .core-circle:before; color: $color-base; } + .text-#{$color-name} { + color: $color-base; + } } + +.accesshide { + position: absolute; + left: -10000px; + font-weight: normal; + font-size: 1em; +} + +.core-monospaced { + font-family: Andale Mono,Monaco,Courier New,DejaVu Sans Mono,monospace; +} + + +.core-white-push-arrow .item-inner{ + background-image: url("data:image/svg+xml;charset=utf-8,") !important; +} + +[ion-fixed] { + width: 100%; +} \ No newline at end of file diff --git a/src/app/fontawesome.scss b/src/app/fontawesome.scss new file mode 100644 index 000000000..6f1ceafb1 --- /dev/null +++ b/src/app/fontawesome.scss @@ -0,0 +1,46 @@ +/** This file is intended to translate fontawesome to ionicons while the font is not supported */ + +.fa.icon { + @extend ion-icon; +} + +/*.fa { + font-size: $button-icon-size; + width: $button-icon-size; + height: $button-icon-size; +}*/ + +/** Fixed width */ +.fa-fw { + width: (18em / 14); + text-align: center; +} + +/** Uncomment for dev purposes, it will show an asterisk in red where a missing icon is */ +/*.fa:before { + color: red !important; + content: $ionicon-var-asterisk; +}*/ + +/** Icons translation */ +.md { + .fa-search-plus:before { content: "\f375"; } // search + .fa-cog:before { content: "\f2cf"; } // cog + .fa-trash:before { content: "\f398"; } // trash + .fa-thumbs-up:before { content: "\f392"; } // thumbs-up + .fa-thumbs-down:before { content: "\f391"; } // thumbs-down + .fa-ban:before { content: "\f367"; } // remove-circle + .fa-remove:before { content: "\f2c0"; } // close + .fa-check:before { content: "\f2bc"; } // checkmark +} + +.ios, .wp { + .fa-search-plus:before { content: "\f4a5"; } // search + .fa-cog:before { content: "\f412"; } // cog + .fa-trash:before { content: "\f4c5"; } // trash + .fa-thumbs-up:before { content: "\f256"; } // thumbs-up + .fa-thumbs-down:before { content: "\f254"; } // thumbs-down + .fa-ban:before { content: "\f1fb"; } // remove-circle + .fa-remove:before { content: "\f406"; } // close + .fa-check:before { content: "\f3ff"; } // checkmark +} diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index aa413dbcc..d9fc0d477 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreAppProvider } from '@providers/app'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import * as moment from 'moment'; /** * Base class to create sync providers. It provides some common functions. @@ -44,10 +46,14 @@ export class CoreSyncBaseProvider { // Store sync promises. protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise } } = {}; - constructor(component: string, protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, + // 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) { - this.logger = this.loggerProvider.getInstance(component); + protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService) { + + this.logger = loggerProvider.getInstance(component); this.component = component; } @@ -93,6 +99,33 @@ export class CoreSyncBaseProvider { } } + /** + * Get the synchronization time in a human readable format. + * + * @param {string | number} id Unique sync identifier per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the readable time. + */ + getReadableSyncTime(id: string | number, siteId?: string): Promise { + return this.getSyncTime(id, siteId).then((time) => { + return this.getReadableTimeFromTimestamp(time); + }); + } + + /** + * Given a timestamp return it in a human readable format. + * + * @param {number} timestamp Timestamp + * @return {string} Human readable time. + */ + getReadableTimeFromTimestamp(timestamp: number): string { + if (!timestamp) { + return this.translate.instant('core.never'); + } else { + return moment(timestamp).format('LLL'); + } + } + /** * Get the synchronization time. Returns 0 if no time stored. * diff --git a/src/classes/delegate.ts b/src/classes/delegate.ts index 953de9ac0..412dad239 100644 --- a/src/classes/delegate.ts +++ b/src/classes/delegate.ts @@ -83,6 +83,12 @@ export class CoreDelegate { */ protected handlerNameProperty = 'name'; + /** + * Set of promises to update a handler, to prevent doing the same operation twice. + * @type {{[siteId: string]: {[name: string]: Promise}}} + */ + protected updatePromises: {[siteId: string]: {[name: string]: Promise}} = {}; + /** * Constructor of the Delegate. * @@ -215,6 +221,13 @@ export class CoreDelegate { currentSite = this.sitesProvider.getCurrentSite(); let promise; + if (this.updatePromises[siteId] && this.updatePromises[siteId][handler.name]) { + // There's already an update ongoing for this handler, return the promise. + return this.updatePromises[siteId][handler.name]; + } else if (!this.updatePromises[siteId]) { + this.updatePromises[siteId] = {}; + } + if (!this.sitesProvider.isLoggedIn()) { promise = Promise.reject(null); } else if (this.isFeatureDisabled(handler, currentSite)) { @@ -224,7 +237,7 @@ export class CoreDelegate { } // Checks if the handler is enabled. - return promise.catch(() => { + this.updatePromises[siteId][handler.name] = promise.catch(() => { return false; }).then((enabled: boolean) => { // Verify that this call is the last one that was started. @@ -236,7 +249,12 @@ export class CoreDelegate { delete this.enabledHandlers[handler[this.handlerNameProperty]]; } } + }).finally(() => { + // Update finished, delete the promise. + delete this.updatePromises[siteId][handler.name]; }); + + return this.updatePromises[siteId][handler.name]; } /** diff --git a/src/classes/sqlitedb.ts b/src/classes/sqlitedb.ts index c60833ce2..8e650767b 100644 --- a/src/classes/sqlitedb.ts +++ b/src/classes/sqlitedb.ts @@ -343,6 +343,10 @@ export class SQLiteDB { * @param {object} data Data to insert. */ protected formatDataToInsert(data: object): void { + if (!data) { + return; + } + // Remove undefined entries and convert null to "NULL". for (const name in data) { const value = data[name]; @@ -782,6 +786,8 @@ export class SQLiteDB { */ updateRecords(table: string, data: any, conditions?: any): Promise { + this.formatDataToInsert(data); + if (!data || !Object.keys(data).length) { // No fields to update, consider it's done. return Promise.resolve(); @@ -792,8 +798,6 @@ export class SQLiteDB { let sql, params; - this.formatDataToInsert(data); - for (const key in data) { sets.push(`${key} = ?`); } diff --git a/src/components/components.module.ts b/src/components/components.module.ts index ff4ae9592..2ede4477b 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -40,6 +40,7 @@ import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; import { CoreSendMessageFormComponent } from './send-message-form/send-message-form'; +import { CoreTimerComponent } from './timer/timer'; @NgModule({ declarations: [ @@ -65,7 +66,8 @@ import { CoreSendMessageFormComponent } from './send-message-form/send-message-f CoreRichTextEditorComponent, CoreNavBarButtonsComponent, CoreDynamicComponent, - CoreSendMessageFormComponent + CoreSendMessageFormComponent, + CoreTimerComponent ], entryComponents: [ CoreContextMenuPopoverComponent, @@ -98,7 +100,8 @@ import { CoreSendMessageFormComponent } from './send-message-form/send-message-f CoreRichTextEditorComponent, CoreNavBarButtonsComponent, CoreDynamicComponent, - CoreSendMessageFormComponent + CoreSendMessageFormComponent, + CoreTimerComponent ] }) export class CoreComponentsModule {} diff --git a/src/components/dynamic-component/dynamic-component.ts b/src/components/dynamic-component/dynamic-component.ts index c41afc529..1692b7663 100644 --- a/src/components/dynamic-component/dynamic-component.ts +++ b/src/components/dynamic-component/dynamic-component.ts @@ -112,7 +112,7 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { * @param {any[]} params List of params to send to the function. * @return {any} Result of the call. Undefined if no component instance or the function doesn't exist. */ - callComponentFunction(name: string, params: any[]): any { + callComponentFunction(name: string, params?: any[]): any { if (this.instance && typeof this.instance[name] == 'function') { return this.instance[name].apply(this.instance, params); } diff --git a/src/components/file/file.ts b/src/components/file/file.ts index 7ed4d61c6..ab0585190 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -158,14 +158,17 @@ export class CoreFileComponent implements OnInit, OnDestroy { promise = this.fileSize ? this.domUtils.confirmDownloadSize({ size: this.fileSize, total: true }) : Promise.resolve(); promise.then(() => { // User confirmed, add the file to queue. - this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl).finally(() => { + return this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl).finally(() => { this.isDownloading = true; + this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component, this.componentId, this.timemodified, undefined, undefined, 0, this.file).catch((error) => { this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); this.calculateState(); }); }); + }).catch(() => { + // Ignore error. }); } } diff --git a/src/components/local-file/local-file.ts b/src/components/local-file/local-file.ts index 6f7dbb871..1acdcbd5d 100644 --- a/src/components/local-file/local-file.ts +++ b/src/components/local-file/local-file.ts @@ -171,15 +171,14 @@ export class CoreLocalFileComponent implements OnInit { // Ask confirmation. this.domUtils.showConfirm(this.translate.instant('core.confirmdeletefile')).then(() => { const modal = this.domUtils.showModalLoading(); - this.fileProvider.removeFile(this.relativePath).then(() => { + + return this.fileProvider.removeFile(this.relativePath).then(() => { this.onDelete.emit(); - }).catch(() => { - this.domUtils.showErrorModal('core.errordeletefile', true); }).finally(() => { modal.dismiss(); }); }).catch(() => { - // User cancelled. + this.domUtils.showErrorModal('core.errordeletefile', true); }); } } diff --git a/src/components/rich-text-editor/rich-text-editor.html b/src/components/rich-text-editor/rich-text-editor.html index 72eee6e14..e4a71afd3 100644 --- a/src/components/rich-text-editor/rich-text-editor.html +++ b/src/components/rich-text-editor/rich-text-editor.html @@ -20,7 +20,7 @@
- +
diff --git a/src/components/rich-text-editor/rich-text-editor.scss b/src/components/rich-text-editor/rich-text-editor.scss index 096779bb8..a850981f3 100644 --- a/src/components/rich-text-editor/rich-text-editor.scss +++ b/src/components/rich-text-editor/rich-text-editor.scss @@ -2,6 +2,7 @@ core-rich-text-editor { height: 40vh; overflow: hidden; min-height: 30vh; + width: 100%; > div { height: 100%; diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index 97a6a6530..c0a9ed6c4 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -42,6 +42,7 @@ export class CoreRichTextEditorComponent { @Input() placeholder = ''; // Placeholder to set in textarea. @Input() control: FormControl; // Form control. + @Input() name = 'core-rich-text-editor'; // Name to set to the textarea. @Output() contentChanged: EventEmitter; @ViewChild('editor') editor: ElementRef; // WYSIWYG editor. @@ -109,6 +110,7 @@ export class CoreRichTextEditorComponent { this.clearText(); } else { this.control.setValue(this.editorElement.innerHTML); + this.textarea.value = this.editorElement.innerHTML; } } else { if (this.isNullOrWhiteSpace(this.textarea.value)) { @@ -117,6 +119,7 @@ export class CoreRichTextEditorComponent { this.control.setValue(this.textarea.value); } } + this.contentChanged.emit(this.control.value); } diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 0734f63eb..9ce98db9a 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -179,16 +179,12 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { * @param {any} e Scroll event. */ showHideTabs(e: any): void { - if (e.target.scrollTop < this.tabBarHeight) { - if (!this.tabsShown) { - this.tabBarElement.classList.remove('tabs-hidden'); - this.tabsShown = true; - } - } else { - if (this.tabsShown) { - this.tabBarElement.classList.add('tabs-hidden'); - this.tabsShown = false; - } + if (this.tabsShown && e.target.scrollTop - this.tabBarHeight > this.tabBarHeight) { + this.tabBarElement.classList.add('tabs-hidden'); + this.tabsShown = false; + } else if (!this.tabsShown && e.target.scrollTop < this.tabBarHeight) { + this.tabBarElement.classList.remove('tabs-hidden'); + this.tabsShown = true; } } diff --git a/src/components/timer/timer.html b/src/components/timer/timer.html new file mode 100644 index 000000000..31262562e --- /dev/null +++ b/src/components/timer/timer.html @@ -0,0 +1,6 @@ + + + + {{ timeLeft | coreSecondsToHMS }} + {{ 'core.timesup' | translate }} + diff --git a/src/components/timer/timer.scss b/src/components/timer/timer.scss new file mode 100644 index 000000000..16f119724 --- /dev/null +++ b/src/components/timer/timer.scss @@ -0,0 +1,26 @@ +core-timer { + .item.item-block .item-inner { + border: 0; + } + + .core-timer { + background-color: transparent; + + span { + font-weight: bold; + } + + // Create the timer warning colors. Go to $core-timer-warn-color. + @for $i from 0 through $core-timer-iterations { + &.core-timer-timeleft-#{$i} { + background-color: rgba($core-timer-warn-color, 1 - ($i / $core-timer-iterations)); + + @if $i <= $core-timer-iterations / 2 { + label, span, ion-icon { + color: $white; + } + } + } + } + } +} diff --git a/src/components/timer/timer.ts b/src/components/timer/timer.ts new file mode 100644 index 000000000..3fb267a34 --- /dev/null +++ b/src/components/timer/timer.ts @@ -0,0 +1,86 @@ +// (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, Output, EventEmitter, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; + +/** + * This directive shows a timer in format HH:MM:SS. When the countdown reaches 0, a function is called. + * + * Usage: + * + */ +@Component({ + selector: 'core-timer', + templateUrl: 'timer.html' +}) +export class CoreTimerComponent implements OnInit, OnDestroy { + @Input() endTime: string | number; // Timestamp (in seconds) when the timer should end. + @Input() timerText: string; // Text to show next to the timer. If not defined, no text shown. + @Input() timeLeftClass: string; // Name of the class to apply with each second. By default, 'core-timer-timeleft-'. + @Input() align: string; // Where to align the time and text. Defaults to 'left'. Other values: 'center', 'right'. + @Output() finished: EventEmitter; // Will emit an event when the timer reaches 0. + + @ViewChild('container', { read: ElementRef }) containerRef: ElementRef; + + timeLeft: number; // Seconds left to end. + + protected timeInterval; + protected container: HTMLElement; + + constructor(protected timeUtils: CoreTimeUtilsProvider) { + this.finished = new EventEmitter(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + const timeLeftClass = this.timeLeftClass || 'core-timer-timeleft-', + endTime = Math.round(Number(this.endTime)), + container: HTMLElement = this.containerRef && this.containerRef.nativeElement; + + if (!endTime) { + return; + } + + // Check time left every 200ms. + this.timeInterval = setInterval(() => { + this.timeLeft = endTime - this.timeUtils.timestamp(); + + if (this.timeLeft < 0) { + // Time is up! Stop the timer and call the finish function. + clearInterval(this.timeInterval); + this.finished.emit(); + + return; + } + + // If the time has nearly expired, change the color. + if (this.timeLeft < 100 && !container.classList.contains(timeLeftClass + this.timeLeft)) { + // Time left has changed. Remove previous classes and add the new one. + container.classList.remove(timeLeftClass + (this.timeLeft + 1)); + container.classList.remove(timeLeftClass + (this.timeLeft + 2)); + container.classList.add(timeLeftClass + this.timeLeft); + } + }, 200); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + clearInterval(this.timeInterval); + } +} diff --git a/src/core/contentlinks/classes/base-handler.ts b/src/core/contentlinks/classes/base-handler.ts index 1b29c509d..9f79ddbda 100644 --- a/src/core/contentlinks/classes/base-handler.ts +++ b/src/core/contentlinks/classes/base-handler.ts @@ -68,7 +68,7 @@ export class CoreContentLinksHandlerBase implements CoreContentLinksHandler { * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. */ getActions(siteIds: string[], url: string, params: any, courseId?: number): - CoreContentLinksAction[] | Promise { + CoreContentLinksAction[] | Promise { return []; } diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index 2eb8be01f..8cd2660d8 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -233,6 +233,8 @@ export class CoreContentLinksHelperProvider { } else { this.goToChooseSite(url); } + }).catch(() => { + // User canceled. }); } diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts index 8af38dfbc..ca2d0abd2 100644 --- a/src/core/course/classes/main-activity-component.ts +++ b/src/core/course/classes/main-activity-component.ts @@ -15,6 +15,7 @@ import { Injector } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreEventsProvider } from '@providers/events'; import { Network } from '@ionic-native/network'; import { CoreAppProvider } from '@providers/app'; @@ -33,8 +34,10 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR protected siteId: string; // Current Site ID. protected syncObserver: any; // It will observe the sync auto event. + protected statusObserver: any; // It will observe changes on the status of the activity. Only if setStatusListener is called. protected onlineObserver: any; // It will observe the status of the network connection. protected syncEventName: string; // Auto sync event name. + protected currentStatus: string; // The current status of the activity. Only if setStatusListener is called. // 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. @@ -42,6 +45,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR protected courseProvider: CoreCourseProvider; protected appProvider: CoreAppProvider; protected eventsProvider: CoreEventsProvider; + protected modulePrefetchProvider: CoreCourseModulePrefetchDelegate; constructor(injector: Injector) { super(injector); @@ -50,6 +54,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR this.courseProvider = injector.get(CoreCourseProvider); this.appProvider = injector.get(CoreAppProvider); this.eventsProvider = injector.get(CoreEventsProvider); + this.modulePrefetchProvider = injector.get(CoreCourseModulePrefetchDelegate); const network = injector.get(Network); @@ -75,6 +80,10 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR this.syncObserver = this.eventsProvider.on(this.syncEventName, (data) => { if (this.isRefreshSyncNeeded(data)) { // Refresh the data. + this.loaded = false; + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + this.refreshContent(false); } }, this.siteId); @@ -168,6 +177,40 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR }); } + /** + * Displays some data based on the current status. + * + * @param {string} status The current status. + * @param {string} [previousStatus] The previous status. If not defined, there is no previous status. + */ + protected showStatus(status: string, previousStatus?: string): void { + // To be overridden. + } + + /** + * Watch for changes on the status. + */ + protected setStatusListener(): void { + if (typeof this.statusObserver == 'undefined') { + // Listen for changes on this module status. + this.statusObserver = this.eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => { + if (data.componentId === this.module.id && data.component === this.component) { + // The status has changed, update it. + const previousStatus = this.currentStatus; + this.currentStatus = data.status; + + this.showStatus(this.currentStatus, previousStatus); + } + }, this.siteId); + + // Also, get the current status. + this.modulePrefetchProvider.getModuleStatus(this.module, this.courseId).then((status) => { + this.currentStatus = status; + this.showStatus(status); + }); + } + } + /** * Performs the sync of the activity. * @@ -217,5 +260,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR this.onlineObserver && this.onlineObserver.unsubscribe(); this.syncObserver && this.syncObserver.off(); + this.statusObserver && this.statusObserver.off(); } } diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts index be9c687e7..48a093320 100644 --- a/src/core/course/classes/main-resource-component.ts +++ b/src/core/course/classes/main-resource-component.ts @@ -42,6 +42,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected isDestroyed; // Whether the component is destroyed, used when calling fillContextMenu. protected statusObserver; // Observer of package status changed, used when calling fillContextMenu. protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents. + protected isCurrentView: boolean; // Whether the component is in the current view. // 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. @@ -174,4 +175,18 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, this.isDestroyed = true; this.statusObserver && this.statusObserver.off(); } + + /** + * User entered the page that contains the component. This function should be called by the page that contains this component. + */ + ionViewDidEnter(): void { + this.isCurrentView = true; + } + + /** + * User left the page that contains the component. This function should be called by the page that contains this component. + */ + ionViewDidLeave(): void { + this.isCurrentView = false; + } } diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 504f5d0a3..d65a8d674 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -326,4 +326,22 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.sectionStatusObserver.off(); } } + + /** + * User entered the page that contains the component. + */ + ionViewDidEnter(): void { + this.dynamicComponents.forEach((component) => { + component.callComponentFunction('ionViewDidEnter'); + }); + } + + /** + * User left the page that contains the component. + */ + ionViewDidLeave(): void { + this.dynamicComponents.forEach((component) => { + component.callComponentFunction('ionViewDidLeave'); + }); + } } diff --git a/src/core/course/components/module-completion/module-completion.scss b/src/core/course/components/module-completion/module-completion.scss index b0b4a663c..78ab92d36 100644 --- a/src/core/course/components/module-completion/module-completion.scss +++ b/src/core/course/components/module-completion/module-completion.scss @@ -3,5 +3,6 @@ core-course-module-completion a { padding: 5px; width: 30px; vertical-align: middle; + max-width: none; } } \ No newline at end of file diff --git a/src/core/course/components/module/module.ts b/src/core/course/components/module/module.ts index aaee9cb5c..8d9bb1d0d 100644 --- a/src/core/course/components/module/module.ts +++ b/src/core/course/components/module/module.ts @@ -66,6 +66,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { protected prefetchHandler: CoreCourseModulePrefetchHandler; protected statusObserver; + protected isDestroyed = false; constructor(@Optional() protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider, @@ -128,14 +129,13 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { // Get download size to ask for confirm if it's high. this.prefetchHandler.getDownloadSize(module, this.courseId).then((size) => { - this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh).catch((error) => { - // Error or cancelled. - this.spinner = false; - }); + return this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh); }).catch((error) => { - // Error getting download size, hide spinner. + // Error, hide spinner. this.spinner = false; - this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + if (!this.isDestroyed && error) { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + } }); } @@ -150,6 +150,10 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { this.showDownload = status === CoreConstants.NOT_DOWNLOADED; this.showRefresh = status === CoreConstants.OUTDATED || (!this.prefetchDelegate.canCheckUpdates() && status === CoreConstants.DOWNLOADED); + + if (this.module.handlerData.updateStatus) { + this.module.handlerData.updateStatus(status); + } } } @@ -158,5 +162,6 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.statusObserver && this.statusObserver.off(); + this.isDestroyed = true; } } diff --git a/src/core/course/formats/singleactivity/components/singleactivity.ts b/src/core/course/formats/singleactivity/components/singleactivity.ts index a68a60fda..4cfa91918 100644 --- a/src/core/course/formats/singleactivity/components/singleactivity.ts +++ b/src/core/course/formats/singleactivity/components/singleactivity.ts @@ -67,4 +67,18 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges { doRefresh(refresher?: any, done?: () => void): Promise { return Promise.resolve(this.dynamicComponent.callComponentFunction('doRefresh', [refresher, done])); } + + /** + * User entered the page that contains the component. + */ + ionViewDidEnter(): void { + this.dynamicComponent.callComponentFunction('ionViewDidEnter'); + } + + /** + * User left the page that contains the component. + */ + ionViewDidLeave(): void { + this.dynamicComponent.callComponentFunction('ionViewDidLeave'); + } } diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index b5d5806ca..33d30e128 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -283,18 +283,18 @@ export class CoreCourseSectionPage implements OnDestroy { */ prefetchCourse(): void { this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course, this.sections, this.courseHandlers) - .then((downloaded) => { - if (downloaded && this.downloadEnabled) { - // Recalculate the status. - this.courseHelper.calculateSectionsStatus(this.sections, this.course.id).catch(() => { - // Ignore errors (shouldn't happen). - }); - } - }).catch((error) => { - if (!this.isDestroyed) { - this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); - } - }); + .then(() => { + if (this.downloadEnabled) { + // Recalculate the status. + this.courseHelper.calculateSectionsStatus(this.sections, this.course.id).catch(() => { + // Ignore errors (shouldn't happen). + }); + } + }).catch((error) => { + if (!this.isDestroyed) { + this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + } + }); } /** @@ -314,4 +314,18 @@ export class CoreCourseSectionPage implements OnDestroy { this.completionObserver.off(); } } + + /** + * User entered the page. + */ + ionViewDidEnter(): void { + this.formatComponent.ionViewDidEnter(); + } + + /** + * User left the page. + */ + ionViewDidLeave(): void { + this.formatComponent.ionViewDidLeave(); + } } diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 8bc95c52b..02502ef0c 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -249,8 +249,7 @@ export class CoreCourseHelperProvider { * @param {any} course Course to prefetch. * @param {any[]} [sections] List of course sections. * @param {CoreCourseOptionsHandlerToDisplay[]} courseHandlers List of course handlers. - * @return {Promise} Promise resolved with true when the download finishes, resolved with false if user doesn't - * confirm, rejected if an error occurs. + * @return {Promise} Promise resolved when the download finishes, rejected if an error occurs or the user cancels. */ confirmAndPrefetchCourse(iconData: any, course: any, sections?: any[], courseHandlers?: CoreCourseOptionsHandlerToDisplay[]) : Promise { @@ -288,11 +287,8 @@ export class CoreCourseHelperProvider { }, (error): any => { // User cancelled or there was an error calculating the size. iconData.prefetchCourseIcon = initialIcon; - if (error) { - return Promise.reject(error); - } - return false; + return Promise.reject(error); }); }); } @@ -302,9 +298,9 @@ export class CoreCourseHelperProvider { * * @param {any[]} courses List of courses to download. * @param {Function} [onProgress] Function to call everytime a course is downloaded. - * @return {Promise} Resolved with true when downloaded, resolved with false if user cancels, rejected if error. + * @return {Promise} Resolved when downloaded, rejected if error or canceled. */ - confirmAndPrefetchCourses(courses: any[], onProgress?: (data: CoreCourseCoursesProgress) => void): Promise { + confirmAndPrefetchCourses(courses: any[], onProgress?: (data: CoreCourseCoursesProgress) => void): Promise { const siteId = this.sitesProvider.getCurrentSiteId(); // Confirm the download without checking size because it could take a while. @@ -347,12 +343,7 @@ export class CoreCourseHelperProvider { onProgress({ count: 0, total: total, success: true }); } - return this.utils.allPromises(promises).then(() => { - return true; - }); - }, () => { - // User cancelled. - return false; + return this.utils.allPromises(promises); }); } @@ -426,23 +417,18 @@ export class CoreCourseHelperProvider { */ contextMenuPrefetch(instance: any, module: any, courseId: number): Promise { const initialIcon = instance.prefetchStatusIcon; - let cancelled = false; instance.prefetchStatusIcon = 'spinner'; // Show spinner since this operation might take a while. // We need to call getDownloadSize, the package might have been updated. return this.prefetchDelegate.getModuleDownloadSize(module, courseId, true).then((size) => { - return this.domUtils.confirmDownloadSize(size).catch(() => { - // User hasn't confirmed, stop. - cancelled = true; - - return Promise.reject(null); - }).then(() => { + return this.domUtils.confirmDownloadSize(size).then(() => { return this.prefetchDelegate.prefetchModule(module, courseId, true); }); }).catch((error) => { instance.prefetchStatusIcon = initialIcon; - if (!instance.isDestroyed && !cancelled) { + + if (!instance.isDestroyed) { this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); } }); @@ -979,36 +965,37 @@ export class CoreCourseHelperProvider { // First of all, mark the course as being downloaded. this.courseDwnPromises[siteId][course.id] = this.courseProvider.setCourseStatus(course.id, CoreConstants.DOWNLOADING, - siteId).then(() => { - const promises = []; - let allSectionsSection = sections[0]; + siteId).then(() => { - // Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections". - if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) { - allSectionsSection = { id: CoreCourseProvider.ALL_SECTIONS_ID }; + const promises = []; + let allSectionsSection = sections[0]; + + // Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections". + if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) { + allSectionsSection = { id: CoreCourseProvider.ALL_SECTIONS_ID }; + } + promises.push(this.prefetchSection(allSectionsSection, course.id, sections)); + + // Prefetch course options. + courseHandlers.forEach((handler) => { + if (handler.prefetch) { + promises.push(handler.prefetch(course)); } - promises.push(this.prefetchSection(allSectionsSection, course.id, sections)); - - // Prefetch course options. - courseHandlers.forEach((handler) => { - if (handler.prefetch) { - promises.push(handler.prefetch(course)); - } - }); - - return this.utils.allPromises(promises); - }).then(() => { - // Download success, mark the course as downloaded. - return this.courseProvider.setCourseStatus(course.id, CoreConstants.DOWNLOADED, siteId); - }).catch((error) => { - // Error, restore previous status. - return this.courseProvider.setCoursePreviousStatus(course.id, siteId).then(() => { - return Promise.reject(error); - }); - }).finally(() => { - delete this.courseDwnPromises[siteId][course.id]; }); + return this.utils.allPromises(promises); + }).then(() => { + // Download success, mark the course as downloaded. + return this.courseProvider.setCourseStatus(course.id, CoreConstants.DOWNLOADED, siteId); + }).catch((error) => { + // Error, restore previous status. + return this.courseProvider.setCoursePreviousStatus(course.id, siteId).then(() => { + return Promise.reject(error); + }); + }).finally(() => { + delete this.courseDwnPromises[siteId][course.id]; + }); + return this.courseDwnPromises[siteId][course.id]; } diff --git a/src/core/course/providers/module-delegate.ts b/src/core/course/providers/module-delegate.ts index 3d46aadae..530fd0ec1 100644 --- a/src/core/course/providers/module-delegate.ts +++ b/src/core/course/providers/module-delegate.ts @@ -107,6 +107,13 @@ export interface CoreCourseModuleHandlerData { * @param {NavOptions} [options] Options for the navigation. */ action?(event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions): void; + + /** + * Updates the status of the module. + * + * @param {string} status Module status. + */ + updateStatus?(status: string): void; } /** diff --git a/src/core/courses/pages/my-courses/my-courses.ts b/src/core/courses/pages/my-courses/my-courses.ts index 1508052d4..98ec7007d 100644 --- a/src/core/courses/pages/my-courses/my-courses.ts +++ b/src/core/courses/pages/my-courses/my-courses.ts @@ -160,9 +160,9 @@ export class CoreCoursesMyCoursesPage implements OnDestroy { return this.courseHelper.confirmAndPrefetchCourses(this.courses, (progress) => { this.prefetchCoursesData.badge = progress.count + ' / ' + progress.total; - }).then((downloaded) => { - this.prefetchCoursesData.icon = downloaded ? 'ion-android-refresh' : initialIcon; - }, (error) => { + }).then(() => { + this.prefetchCoursesData.icon = 'ion-android-refresh'; + }).catch((error) => { if (!this.isDestroyed) { this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); this.prefetchCoursesData.icon = initialIcon; diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts index 552208dfa..8d609ed4e 100644 --- a/src/core/courses/pages/my-overview/my-overview.ts +++ b/src/core/courses/pages/my-overview/my-overview.ts @@ -362,9 +362,9 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { return this.courseHelper.confirmAndPrefetchCourses(this.courses[selected], (progress) => { selectedData.badge = progress.count + ' / ' + progress.total; - }).then((downloaded) => { - selectedData.icon = downloaded ? 'refresh' : initialIcon; - }, (error) => { + }).then(() => { + selectedData.icon = 'refresh'; + }).catch((error) => { if (!this.isDestroyed) { this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); selectedData.icon = initialIcon; diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 6b45c3571..8f20a9b08 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -235,7 +235,7 @@ export class CoreLoginHelperProvider { promise.then(() => { this.openBrowserForSSOLogin(siteUrl, typeOfLogin, service, launchUrl); - }, () => { + }).catch(() => { // User cancelled, ignore. }); } diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts index 5befc6391..c86b7abe4 100644 --- a/src/core/question/classes/base-question-component.ts +++ b/src/core/question/classes/base-question-component.ts @@ -83,7 +83,6 @@ export class CoreQuestionBaseComponent { if (optionEl.selected) { selectModel.selected = option.value; - selectModel.selectedLabel = option.label; } selectModel.options.push(option); @@ -92,7 +91,6 @@ export class CoreQuestionBaseComponent { if (!selectModel.selected) { // No selected option, select the first one. selectModel.selected = selectModel.options[0].value; - selectModel.selectedLabel = selectModel.options[0].label; } // Get the accessibility label. @@ -158,7 +156,7 @@ export class CoreQuestionBaseComponent { // Check which one should be displayed first: the options or the input. const input = questionDiv.querySelector('input[type="text"][name*=answer]'); this.question.optionsFirst = - questionDiv.innerHTML.indexOf(input.outerHTML) > questionDiv.innerHTML.indexOf(options[0].outerHTML); + questionDiv.innerHTML.indexOf(input.outerHTML) > questionDiv.innerHTML.indexOf(radios[0].outerHTML); } } @@ -263,7 +261,7 @@ export class CoreQuestionBaseComponent { this.questionHelper.replaceFeedbackClasses(div); // Treat the correct/incorrect icons. - this.questionHelper.treatCorrectnessIcons(div, this.component, this.componentId); + this.questionHelper.treatCorrectnessIcons(div); // Set the question text. this.question.text = content.innerHTML; @@ -313,7 +311,7 @@ export class CoreQuestionBaseComponent { if (questionDiv) { // Find rows. - const rows = Array.from(questionDiv.querySelectorAll('tr')); + const rows = Array.from(questionDiv.querySelectorAll('table.answer tr')); if (!rows || !rows.length) { this.logger.warn('Aborting because couldn\'t find any row.', this.question.name); @@ -376,7 +374,7 @@ export class CoreQuestionBaseComponent { }; if (option.selected) { - rowModel.selected = option; + rowModel.selected = option.value; } rowModel.options.push(option); @@ -404,8 +402,6 @@ export class CoreQuestionBaseComponent { const questionDiv = this.initComponent(); if (questionDiv) { - // Create the model for radio buttons. - this.question.singleChoiceModel = {}; // Get the prompt. this.question.prompt = this.domUtils.getContentsOfElement(questionDiv, '.prompt'); @@ -481,6 +477,11 @@ export class CoreQuestionBaseComponent { return this.questionHelper.showComponentError(this.onAbort); } + + if (!this.question.multi && typeof this.question.singleChoiceModel == 'undefined') { + // We couldn't find the option to select, select the first one. + this.question.singleChoiceModel = options[0].value; + } } return questionDiv; diff --git a/src/core/question/components/question/question.html b/src/core/question/components/question/question.html index 33fc72525..4123240fd 100644 --- a/src/core/question/components/question/question.html +++ b/src/core/question/components/question/question.html @@ -12,13 +12,13 @@ - +

{{ question.validationError }}

- {{ button.value }} + diff --git a/src/core/question/components/question/question.ts b/src/core/question/components/question/question.ts index ddfb7ecea..57913acc9 100644 --- a/src/core/question/components/question/question.ts +++ b/src/core/question/components/question/question.ts @@ -123,7 +123,7 @@ export class CoreQuestionComponent implements OnInit { promise.then(() => { // Handle behaviour. - this.behaviourDelegate.handleQuestion(this.question, this.question.preferredBehaviour).then((comps) => { + this.behaviourDelegate.handleQuestion(this.question.preferredBehaviour, this.question).then((comps) => { this.behaviourComponents = comps; }); this.questionHelper.extractQbehaviourRedoButton(this.question); diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index f6d317dd2..b73dd9823 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -14,10 +14,14 @@ import { Injectable, EventEmitter } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreQuestionProvider } from './question'; +import { CoreQuestionDelegate } from './delegate'; /** * Service with some common functions to handle questions. @@ -29,7 +33,8 @@ export class CoreQuestionHelperProvider { constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, private questionProvider: CoreQuestionProvider, private sitesProvider: CoreSitesProvider, - private translate: TranslateService) { } + private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, private utils: CoreUtilsProvider, + private filepoolProvider: CoreFilepoolProvider, private questionDelegate: CoreQuestionDelegate) { } /** * Add a behaviour button to the question's "behaviourButtons" property. @@ -298,6 +303,44 @@ export class CoreQuestionHelperProvider { return answers; } + /** + * Retrieve the answers entered in a form. + * We don't use ngModel because it doesn't detect changes done by JavaScript and some questions might do that. + * + * @param {HTMLFormElement} form Form. + * @return {any} Object with the answers. + */ + getAnswersFromForm(form: HTMLFormElement): any { + if (!form || !form.elements) { + return {}; + } + + const answers = {}, + elements = Array.from(form.elements); + + elements.forEach((element: HTMLInputElement) => { + const name = element.name || element.getAttribute('ng-reflect-name') || ''; + + // Ignore flag and submit inputs. + if (!name || name.match(/_:flagged$/) || element.type == 'submit' || element.tagName == 'BUTTON') { + return; + } + + // Get the value. + if (element.type == 'checkbox') { + answers[name] = !!element.checked; + } else if (element.type == 'radio') { + if (element.checked) { + answers[name] = element.value; + } + } else { + answers[name] = element.value; + } + }); + + return answers; + } + /** * Given an HTML code with list of attachments, returns the list of attached files (filename and fileurl). * Please take into account that this function will treat all the anchors in the HTML, you should provide @@ -353,6 +396,18 @@ export class CoreQuestionHelperProvider { } } + /** + * Get the CSS class for a question based on its state. + * + * @param {string} name Question's state name. + * @return {string} State class. + */ + getQuestionStateClass(name: string): string { + const state = this.questionProvider.getState(name); + + return state ? state.class : ''; + } + /** * Get the validation error message from a question HTML if it's there. * @@ -428,6 +483,64 @@ export class CoreQuestionHelperProvider { question.html = form.innerHTML; } + /** + * Prefetch the files in a question HTML. + * + * @param {any} question Question. + * @param {string} [component] The component to link the files to. If not defined, question component. + * @param {string|number} [componentId] An ID to use in conjunction with the component. If not defined, question ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when all the files have been downloaded. + */ + prefetchQuestionFiles(question: any, component?: string, componentId?: string | number, siteId?: string): Promise { + const urls = this.domUtils.extractDownloadableFilesFromHtml(question.html); + + if (!component) { + component = CoreQuestionProvider.COMPONENT; + componentId = question.id; + } + + return this.sitesProvider.getSite(siteId).then((site) => { + const promises = []; + + urls.forEach((url) => { + if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) { + return; + } + + if (url.indexOf('theme/image.php') > -1 && url.indexOf('flagged') > -1) { + // Ignore flag images. + return; + } + + promises.push(this.filepoolProvider.addToQueueByUrl(siteId, url, component, componentId)); + }); + + return Promise.all(promises); + }); + } + + /** + * Prepare and return the answers. + * + * @param {any[]} questions The list of questions. + * @param {any} answers The input data. + * @param {boolean} offline True if data should be saved in offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with answers to send to server. + */ + prepareAnswers(questions: any[], answers: any, offline?: boolean, siteId?: string): Promise { + const promises = []; + + questions.forEach((question) => { + promises.push(this.questionDelegate.prepareAnswersForQuestion(question, answers, offline, siteId)); + }); + + return this.utils.allPromises(promises).then(() => { + return answers; + }); + } + /** * Replace Moodle's correct/incorrect classes with the Mobile ones. * @@ -487,13 +600,11 @@ export class CoreQuestionHelperProvider { * @param {string} [error] Error to show. */ showComponentError(onAbort: EventEmitter, error?: string): void { - error = error || 'Error processing the question. This could be caused by custom modifications in your site.'; - // Prevent consecutive errors. const now = Date.now(); if (now - this.lastErrorShown > 500) { this.lastErrorShown = now; - this.domUtils.showErrorModal(error); + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorparsequestions', true); } onAbort && onAbort.emit(); @@ -504,7 +615,7 @@ export class CoreQuestionHelperProvider { * * @param {HTMLElement} element DOM element. */ - treatCorrectnessIcons(element: HTMLElement, component?: string, componentId?: number): void { + treatCorrectnessIcons(element: HTMLElement): void { const icons = Array.from(element.querySelectorAll('img.icon, img.questioncorrectnessicon')); icons.forEach((icon) => { @@ -541,9 +652,31 @@ export class CoreQuestionHelperProvider { icon.classList.add('questioncorrectnessicon'); if (span.innerHTML) { + // There's a hidden feedback. Mark the icon as tappable. + // The click listener is only added if treatCorrectnessIconsClicks is called. + icon.setAttribute('tappable', ''); + } + }); + } + + /** + * Add click listeners to all tappable correctness icons. + * + * @param {HTMLElement} element DOM element. + * @param {string} [component] The component to use when viewing the feedback. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + */ + treatCorrectnessIconsClicks(element: HTMLElement, component?: string, componentId?: number): void { + const icons = Array.from(element.querySelectorAll('i.icon.questioncorrectnessicon[tappable]')), + title = this.translate.instant('core.question.feedback'); + + icons.forEach((icon) => { + // Search the feedback for the icon. + const span = icon.parentElement.querySelector('.feedbackspan.accesshide'); + + if (span) { // There's a hidden feedback, show it when the icon is clicked. icon.addEventListener('click', (event) => { - const title = this.translate.instant('core.question.feedback'); this.textUtils.expandText(title, span.innerHTML, component, componentId); }); } diff --git a/src/core/question/providers/question.ts b/src/core/question/providers/question.ts index 9dd4aa95a..e7d81d630 100644 --- a/src/core/question/providers/question.ts +++ b/src/core/question/providers/question.ts @@ -58,6 +58,8 @@ export interface CoreQuestionState { */ @Injectable() export class CoreQuestionProvider { + static COMPONENT = 'mmQuestion'; + // Variables for database. protected QUESTION_TABLE = 'questions'; protected QUESTION_ANSWERS_TABLE = 'question_answers'; @@ -403,7 +405,8 @@ export class CoreQuestionProvider { */ getQuestionAnswers(component: string, attemptId: number, slot: number, filter?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, slot}).then((answers) => { + return site.getDb().getRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, questionSlot: slot}) + .then((answers) => { if (filter) { // Get only answers that isn't "extra" data like sequencecheck or certainty. return this.getBasicAnswersFromArray(answers); @@ -523,7 +526,7 @@ export class CoreQuestionProvider { */ removeQuestionAnswers(component: string, attemptId: number, slot: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().deleteRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, slot}); + return site.getDb().deleteRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, questionSlot: slot}); }); } diff --git a/src/core/sitehome/pages/index/index.scss b/src/core/sitehome/pages/index/index.scss new file mode 100644 index 000000000..f16d8c3e3 --- /dev/null +++ b/src/core/sitehome/pages/index/index.scss @@ -0,0 +1,5 @@ +page-core-sitehome-index { + ion-header { + position: relative; + } +} \ No newline at end of file diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index 8663b050f..eb398387c 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -653,10 +653,10 @@ export class CoreFilepoolProvider { if (filePath && entry.path !== filePath) { newData.path = filePath; } - if (entry.isexternalfile !== options.isexternalfile) { + if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) { newData.isexternalfile = options.isexternalfile; } - if (entry.repositorytype !== options.repositorytype) { + if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) { newData.repositorytype = options.repositorytype; } @@ -2659,7 +2659,7 @@ export class CoreFilepoolProvider { // Going back from downloading to previous status, restore previous download time. newData.downloadTime = entry.previousDownloadTime; } - newData.status = entry.previous || CoreConstants.DOWNLOADED; + newData.status = entry.previous || CoreConstants.NOT_DOWNLOADED; newData.updated = Date.now(); this.logger.debug(`Set previous status '${entry.status}' for package ${component} ${componentId}`); diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index f20fca0ed..494e2bd6f 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -139,6 +139,15 @@ export class CoreDomUtilsProvider { return Promise.resolve(); } + /** + * Create a "cancelled" error. These errors won't display an error message in showErrorModal functions. + * + * @return {any} The error object. + */ + createCanceledError(): any { + return {coreCanceled: true}; + } + /** * Extract the downloadable URLs from an HTML code. * @@ -771,7 +780,7 @@ export class CoreDomUtilsProvider { * @param {string} [okText] Text of the OK button. * @param {string} [cancelText] Text of the Cancel button. * @param {any} [options] More options. See https://ionicframework.com/docs/api/components/alert/AlertController/ - * @return {Promise} Promise resolved if the user confirms and rejected if he cancels. + * @return {Promise} Promise resolved if the user confirms and rejected with a canceled error if he cancels. */ showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options?: any): Promise { return new Promise((resolve, reject): void => { @@ -787,7 +796,7 @@ export class CoreDomUtilsProvider { text: cancelText || this.translate.instant('core.cancel'), role: 'cancel', handler: (): void => { - reject(); + reject(this.createCanceledError()); } }, { @@ -813,7 +822,10 @@ export class CoreDomUtilsProvider { showErrorModal(error: any, needsTranslate?: boolean, autocloseTime?: number): Alert { if (typeof error == 'object') { // We received an object instead of a string. Search for common properties. - if (typeof error.content != 'undefined') { + if (error.coreCanceled) { + // It's a canceled error, don't display an error. + return; + } else if (typeof error.content != 'undefined') { error = error.content; } else if (typeof error.body != 'undefined') { error = error.body; @@ -833,6 +845,11 @@ export class CoreDomUtilsProvider { } } + if (error == CoreConstants.DONT_SHOW_ERROR) { + // The error shouldn't be shown, stop. + return; + } + const message = this.textUtils.decodeHTML(needsTranslate ? this.translate.instant(error) : error); return this.showAlert(this.getErrorTitle(message), message, undefined, autocloseTime); @@ -848,14 +865,18 @@ export class CoreDomUtilsProvider { * @return {Alert} The alert modal. */ showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Alert { - if (error != CoreConstants.DONT_SHOW_ERROR) { - if (error && typeof error != 'string') { - error = error.message || error.error; - } - error = typeof error == 'string' ? error : defaultError; - - return this.showErrorModal(error, needsTranslate, autocloseTime); + if (error && error.coreCanceled) { + // It's a canceled error, don't display an error. + return; } + + if (error && typeof error != 'string') { + error = error.message || error.error || error.content || error.body; + } + + error = typeof error == 'string' ? error : defaultError; + + return this.showErrorModal(error, needsTranslate, autocloseTime); } /** diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 9f6fc3d6b..0a67041e9 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -208,6 +208,23 @@ $core-rte-min-height: 80px; $core-toolbar-button-image-width: 32px; +// Timer variables. +$core-timer-warn-color: $red !default; +$core-timer-iterations: 15 !default; + +// Question variables. +$core-question-correct-color: $green-dark !default; +$core-question-correct-color-bg: $green-light !default; +$core-question-incorrect-color: $red !default; +$core-question-incorrect-color-bg: $red-light !default; +$core-question-feedback-color: $yellow-dark !default; +$core-question-feedback-color-bg: $yellow-light !default; +$core-question-warning-color: $red !default; + +$core-question-state-correct-color: $green-light !default; +$core-question-state-partial-color: $yellow-light !default; +$core-question-state-incorrect-color: $red-light !default; + // Mixins // ------------------------- @mixin core-transition($where: all, $time: 500ms) {