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