Merge pull request #1830 from moodlehq/integration

Integration
main
Juan Leyva 2019-03-29 13:44:44 +01:00 committed by GitHub
commit 7ef37cbebf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
419 changed files with 14648 additions and 4168 deletions

2
.dockerignore 100644
View File

@ -0,0 +1,2 @@
node_modules
Dockerfile

8
.gitattributes vendored 100644
View File

@ -0,0 +1,8 @@
# This file has been retrieved from angular repository.
# Auto detect text files and perform LF normalization
* text=auto
# JS and TS files must always use LF for tools to work
*.js eol=lf
*.ts eol=lf

View File

@ -1,16 +1,23 @@
sudo: required sudo: required
dist: trusty dist: xenial
group: edge group: edge
language: node_js language: node_js
node_js: node_js: stable
- '8.10'
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
before_script: before_script:
- npm install -g @angular/cli - npm install -g @angular/cli
- npm i npm@latest -g - npm i npm@latest -g
- gulp - gulp
- rm -Rf node_modules/electron-builder-squirrel-windows node_modules/electron-windows-notifications #Avoid electron fail
script: script:
- npm run build - npm run build

27
Dockerfile 100644
View File

@ -0,0 +1,27 @@
# This image is based on the fat node 11 image.
# We require fat images as neither alpine, or slim, include git binaries.
FROM node:11
# Port 8100 for ionic dev server.
EXPOSE 8100
# Port 35729 is the live-reload server.
EXPOSE 35729
# Port 53703 is the Chrome dev logger port.
EXPOSE 53703
# MoodleMobile uses Cordova, Ionic, and Gulp.
RUN npm install -g cordova ionic gulp && rm -rf /root/.npm
WORKDIR /app
COPY . /app
# The setup script will handle npm installation, cordova setup, and gulp setup.
RUN npm run setup && rm -rf /root/.npm
# Provide a Healthcheck command for easier use in CI.
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s CMD curl -f http://localhost:8100 || exit 1
CMD ["ionic", "serve", "-b"]

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AD_UNIT_ID_FOR_BANNER_TEST</key>
<string>ca-app-pub-3940256099942544/2934735716</string>
<key>AD_UNIT_ID_FOR_INTERSTITIAL_TEST</key>
<string>ca-app-pub-3940256099942544/4411468910</string>
<key>CLIENT_ID</key>
<string>694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr</string>
<key>API_KEY</key>
<string>AIzaSyA-77ZjkxII6GV97CC9rdUl83rzdEXu_rM</string>
<key>GCM_SENDER_ID</key>
<string>694767596569</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.moodle.moodlemobile</string>
<key>PROJECT_ID</key>
<string>moodlemobile-push</string>
<key>STORAGE_BUCKET</key>
<string>moodlemobile-push.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<true></true>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:694767596569:ios:a4cdad4d168c9d1a</string>
<key>DATABASE_URL</key>
<string>https://moodlemobile-push.firebaseio.com</string>
</dict>
</plist>

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <name>Moodle</name>
<description>Moodle official app</description> <description>Moodle official app</description>
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author> <author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
@ -41,6 +41,7 @@
<param name="ios-package" onload="true" value="CDVStatusBar" /> <param name="ios-package" onload="true" value="CDVStatusBar" />
</feature> </feature>
<platform name="android"> <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-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-mdpi" src="resources/android/splash/drawable-land-mdpi-screen.png" />
<splash qualifier="land-hdpi" src="resources/android/splash/drawable-land-hdpi-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="xhdpi" src="resources/android/icon/drawable-xhdpi-icon.png" />
<icon density="xxhdpi" src="resources/android/icon/drawable-xxhdpi-icon.png" /> <icon density="xxhdpi" src="resources/android/icon/drawable-xxhdpi-icon.png" />
<icon density="xxxhdpi" src="resources/android/icon/drawable-xxxhdpi-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>
<platform name="ios"> <platform name="ios">
<resource-file src="GoogleService-Info.plist" />
<icon height="57" src="resources/ios/icon/icon.png" width="57" /> <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="114" src="resources/ios/icon/icon@2x.png" width="114" />
<icon height="40" src="resources/ios/icon/icon-40.png" width="40" /> <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" /> <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" /> <splash height="2732" src="resources/ios/splash/Default@2x~universal~anyany.png" width="2732" />
</platform> </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"> <plugin name="cordova-android-support-gradle-release" spec="2.0.1">
<variable name="ANDROID_SUPPORT_VERSION" value="27.1.0" /> <variable name="ANDROID_SUPPORT_VERSION" value="27.1.0" />
</plugin> </plugin>
@ -111,7 +129,7 @@
<plugin name="cordova-plugin-globalization" spec="1.11.0" /> <plugin name="cordova-plugin-globalization" spec="1.11.0" />
<plugin name="cordova-plugin-inappbrowser" spec="3.0.0" /> <plugin name="cordova-plugin-inappbrowser" spec="3.0.0" />
<plugin name="cordova-plugin-ionic-keyboard" spec="2.1.3" /> <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-media-capture" spec="3.0.2" />
<plugin name="cordova-plugin-network-information" spec="2.0.1" /> <plugin name="cordova-plugin-network-information" spec="2.0.1" />
<plugin name="cordova-plugin-screen-orientation" spec="3.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-plugin-zip" spec="3.1.0" />
<plugin name="cordova-sqlite-storage" spec="2.6.0" /> <plugin name="cordova-sqlite-storage" spec="2.6.0" />
<plugin name="nl.kingsquare.cordova.background-audio" spec="1.0.1" /> <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"> <plugin name="phonegap-plugin-push" spec="https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v2">
<variable name="SENDER_ID" value="694767596569" /> <variable name="ANDROID_SUPPORT_V13_VERSION" value="27.+" />
<variable name="FCM_VERSION" value="17.0.+" />
</plugin> </plugin>
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application/activity[@android:name='MainActivity']"> <edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application/activity[@android:name='MainActivity']">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" android:debuggable="true" /> <activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" android:debuggable="true" />

View File

@ -1,4 +1,4 @@
// New copy task for font files // New copy task for font files and config.json.
module.exports = { module.exports = {
// Override Ionic copyFonts task to exclude Roboto and Noto fonts. // Override Ionic copyFonts task to exclude Roboto and Noto fonts.
copyFonts: { copyFonts: {
@ -8,5 +8,9 @@ module.exports = {
copyFontAwesome: { copyFontAwesome: {
src: ['{{ROOT}}/node_modules/font-awesome/fonts/**/*'], src: ['{{ROOT}}/node_modules/font-awesome/fonts/**/*'],
dest: '{{WWW}}/assets/fonts' dest: '{{WWW}}/assets/fonts'
},
copyConfig: {
src: ['{{ROOT}}/src/config.json'],
dest: '{{WWW}}/'
} }
}; };

View File

@ -6,7 +6,7 @@
<Identity Name="3312ADB7.MoodleDesktop" <Identity Name="3312ADB7.MoodleDesktop"
ProcessorArchitecture="x64" ProcessorArchitecture="x64"
Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6" Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6"
Version="3.6.0.0" /> Version="3.6.1.0" />
<Properties> <Properties>
<DisplayName>Moodle Desktop</DisplayName> <DisplayName>Moodle Desktop</DisplayName>
<PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName> <PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName>

View File

@ -70,6 +70,26 @@ function createWindow() {
mainWindow.webContents.setUserAgent(mainWindow.webContents.getUserAgent() + ' ' + userAgent); 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. // 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. // Some APIs can only be used after this event occurs.
app.on('ready', function() { 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). // Listen for open-url events (Mac OS only).
app.on('open-url', (event, url) => { app.on('open-url', (event, url) => {
event.preventDefault(); event.preventDefault();

View File

@ -0,0 +1,45 @@
{
"project_info": {
"project_number": "694767596569",
"firebase_url": "https://moodlemobile-push.firebaseio.com",
"project_id": "moodlemobile-push",
"storage_bucket": "moodlemobile-push.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:694767596569:android:a4cdad4d168c9d1a",
"android_client_info": {
"package_name": "com.moodle.moodlemobile"
}
},
"oauth_client": [
{
"client_id": "694767596569-icveqqa2n56oh44l6ev1dr2oh67nh8il.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCb2zogu0P_aZ2LNgdwzshWExITPKTXJyk"
},
{
"current_key": "AIzaSyDRT1HwT0gSsTty0whOVtoNKAh8SPrJXLE"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

View File

@ -47,7 +47,13 @@ function treatFile(file, data) {
return; // ignore return; // ignore
} }
try { 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()); data[path] = JSON.parse(file.contents.toString());
} catch (err) { } catch (err) {
console.log('Error parsing JSON: ' + err); console.log('Error parsing JSON: ' + err);
@ -65,7 +71,7 @@ function treatMergedData(data) {
var mergedOrdered = {}; var mergedOrdered = {};
for (var filepath in data) { for (var filepath in data) {
var pathSplit = filepath.split('/'), var pathSplit = filepath.split(/[\/\\]/),
prefix; prefix;
pathSplit.pop(); pathSplit.pop();

View File

@ -1,49 +0,0 @@
#!/usr/bin/env node
// This hook copies Android splash screen files from dev directories into the appropriate platform specific location.
// The code was extracted from here: http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/
var filesToCopy = [{
'resources/android/splash/drawable-land-hdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-hdpi/screen.png'
}, {
'resources/android/splash/drawable-land-ldpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-ldpi/screen.png'
}, {
'resources/android/splash/drawable-land-mdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-mdpi/screen.png'
}, {
'resources/android/splash/drawable-land-xhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png'
}, {
'resources/android/splash/drawable-land-xxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png'
}, {
'resources/android/splash/drawable-land-xxxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png'
}, {
'resources/android/splash/drawable-port-hdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-hdpi/screen.png'
}, {
'resources/android/splash/drawable-port-ldpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-ldpi/screen.png'
}, {
'resources/android/splash/drawable-port-mdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-mdpi/screen.png'
}, {
'resources/android/splash/drawable-port-xhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png'
}, {
'resources/android/splash/drawable-port-xxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png'
}, {
'resources/android/splash/drawable-port-xxxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png'
}
];
var fs = require('fs');
var path = require('path');
// no need to configure below
var rootDir = process.argv[2];
filesToCopy.forEach(function(obj) {
Object.keys(obj).forEach(function(key) {
var val = obj[key];
var srcFile = path.join(rootDir, key);
var destFile = path.join(rootDir, val);
var destDir = path.dirname(destFile);
if (fs.existsSync(srcFile) && fs.existsSync(destDir)) {
fs.createReadStream(srcFile).pipe(fs.createWriteStream(destFile));
}
});
});

18
hooks/post_push 100644
View File

@ -0,0 +1,18 @@
#!/bin/bash
set -e
if [ "integration" != "${SOURCE_BRANCH}" ]
then
# A space-separated list of additional tags to place on this image.
additionalTags=(latest)
# Tag and push image for each additional tag
for tag in ${additionalTags[@]}; do
echo "Tagging {$IMAGE_NAME} as ${DOCKER_REPO}:${tag}"
docker tag $IMAGE_NAME ${DOCKER_REPO}:${tag}
echo "Pushing ${DOCKER_REPO}:${tag}"
docker push ${DOCKER_REPO}:${tag}
done
fi

View File

@ -6,6 +6,5 @@
}, },
"type": "ionic-angular", "type": "ionic-angular",
"watchPatterns": [], "watchPatterns": [],
"pro_id": "com.moodle.moodlemobile",
"id": "com.moodle.moodlemobile" "id": "com.moodle.moodlemobile"
} }

2611
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "moodlemobile", "name": "moodlemobile",
"version": "3.6.0", "version": "3.6.1",
"description": "The official app for Moodle.", "description": "The official app for Moodle.",
"author": { "author": {
"name": "Moodle Pty Ltd.", "name": "Moodle Pty Ltd.",
@ -35,7 +35,7 @@
"preionic:build": "gulp", "preionic:build": "gulp",
"postionic:build": "gulp copy-component-templates", "postionic:build": "gulp copy-component-templates",
"desktop.pack": "electron-builder --dir", "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" "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": { "dependencies": {
@ -59,7 +59,7 @@
"@ionic-native/globalization": "4.17.0", "@ionic-native/globalization": "4.17.0",
"@ionic-native/in-app-browser": "4.17.0", "@ionic-native/in-app-browser": "4.17.0",
"@ionic-native/keyboard": "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/media-capture": "4.17.0",
"@ionic-native/network": "4.17.0", "@ionic-native/network": "4.17.0",
"@ionic-native/push": "4.17.0", "@ionic-native/push": "4.17.0",
@ -83,7 +83,6 @@
"cordova-android-support-gradle-release": "2.0.1", "cordova-android-support-gradle-release": "2.0.1",
"cordova-clipboard": "1.2.1", "cordova-clipboard": "1.2.1",
"cordova-ios": "4.5.5", "cordova-ios": "4.5.5",
"cordova-plugin-app-event": "1.2.1",
"cordova-plugin-badge": "0.8.8", "cordova-plugin-badge": "0.8.8",
"cordova-plugin-camera": "4.0.3", "cordova-plugin-camera": "4.0.3",
"cordova-plugin-customurlscheme": "4.3.0", "cordova-plugin-customurlscheme": "4.3.0",
@ -94,7 +93,7 @@
"cordova-plugin-globalization": "1.11.0", "cordova-plugin-globalization": "1.11.0",
"cordova-plugin-inappbrowser": "3.0.0", "cordova-plugin-inappbrowser": "3.0.0",
"cordova-plugin-ionic-keyboard": "2.1.3", "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-media-capture": "3.0.2",
"cordova-plugin-network-information": "2.0.1", "cordova-plugin-network-information": "2.0.1",
"cordova-plugin-screen-orientation": "3.0.1", "cordova-plugin-screen-orientation": "3.0.1",
@ -103,14 +102,16 @@
"cordova-plugin-whitelist": "1.3.3", "cordova-plugin-whitelist": "1.3.3",
"cordova-plugin-zip": "3.1.0", "cordova-plugin-zip": "3.1.0",
"cordova-sqlite-storage": "2.6.0", "cordova-sqlite-storage": "2.6.0",
"cordova-support-google-services": "1.2.1",
"es6-promise-plugin": "4.2.2", "es6-promise-plugin": "4.2.2",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"ionic-angular": "3.9.2", "ionic-angular": "3.9.3",
"ionicons": "3.0.0", "ionicons": "3.0.0",
"jszip": "3.1.5", "jszip": "3.1.5",
"moment": "2.22.2", "moment": "2.22.2",
"nl.kingsquare.cordova.background-audio": "1.0.1", "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", "promise.prototype.finally": "3.1.0",
"rxjs": "5.5.11", "rxjs": "5.5.11",
"sw-toolbox": "3.6.0", "sw-toolbox": "3.6.0",
@ -119,9 +120,9 @@
"zone.js": "0.8.26" "zone.js": "0.8.26"
}, },
"devDependencies": { "devDependencies": {
"@ionic/app-scripts": "3.1.9", "@ionic/app-scripts": "3.2.2",
"electron-rebuild": "1.8.1",
"electron-builder-lib": "20.23.1", "electron-builder-lib": "20.23.1",
"electron-rebuild": "1.8.1",
"gulp": "4.0.0", "gulp": "4.0.0",
"gulp-clip-empty-files": "0.1.2", "gulp-clip-empty-files": "0.1.2",
"gulp-flatten": "0.4.0", "gulp-flatten": "0.4.0",
@ -159,7 +160,7 @@
"cordova-plugin-globalization": {}, "cordova-plugin-globalization": {},
"cordova-plugin-inappbrowser": {}, "cordova-plugin-inappbrowser": {},
"cordova-plugin-ionic-keyboard": {}, "cordova-plugin-ionic-keyboard": {},
"cordova-plugin-local-notifications-mm": {}, "cordova-plugin-local-notification": {},
"cordova-plugin-media-capture": {}, "cordova-plugin-media-capture": {},
"cordova-plugin-network-information": {}, "cordova-plugin-network-information": {},
"cordova-plugin-screen-orientation": {}, "cordova-plugin-screen-orientation": {},
@ -170,7 +171,8 @@
"cordova-sqlite-storage": {}, "cordova-sqlite-storage": {},
"nl.kingsquare.cordova.background-audio": {}, "nl.kingsquare.cordova.background-audio": {},
"phonegap-plugin-push": { "phonegap-plugin-push": {
"SENDER_ID": "694767596569" "ANDROID_SUPPORT_V13_VERSION": "27.+",
"FCM_VERSION": "17.0.+"
} }
} }
}, },
@ -206,7 +208,7 @@
} }
], ],
"compression": "maximum", "compression": "maximum",
"electronVersion": "2.0.4", "electronVersion": "4.0.1",
"mac": { "mac": {
"category": "public.app-category.education", "category": "public.app-category.education",
"icon": "resources/desktop/icon.icns", "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

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Compile AOT. # 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 cd scripts
./build_lang.sh ./build_lang.sh
cd .. cd ..
@ -38,9 +38,8 @@ fi
# Copy to PGB git (only on a configured travis build). # Copy to PGB git (only on a configured travis build).
if [ ! -z $GIT_ORG ] && [ ! -z $GIT_TOKEN ] ; then if [ ! -z $GIT_ORG ] && [ ! -z $GIT_TOKEN ] ; then
gitfolder=${PWD##*/} gitfolder=${PWD##*/}
cd .. git clone --depth 1 --no-single-branch https://github.com/$GIT_ORG/moodlemobile-phonegapbuild.git ../pgb
git clone --depth 1 --no-single-branch https://github.com/$GIT_ORG/moodlemobile-phonegapbuild.git pgb pushd ../pgb
cd pgb
git checkout $TRAVIS_BRANCH git checkout $TRAVIS_BRANCH
rm -Rf assets build index.html templates rm -Rf assets build index.html templates
cp -Rf ../$gitfolder/www/* ./ cp -Rf ../$gitfolder/www/* ./
@ -48,4 +47,10 @@ if [ ! -z $GIT_ORG ] && [ ! -z $GIT_TOKEN ] ; then
git add . git add .
git commit -m "Travis build: $TRAVIS_BUILD_NUMBER" git commit -m "Travis build: $TRAVIS_BUILD_NUMBER"
git push https://$GIT_TOKEN@github.com/$GIT_ORG/moodlemobile-phonegapbuild.git git push https://$GIT_TOKEN@github.com/$GIT_ORG/moodlemobile-phonegapbuild.git
popd
fi fi
if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] && [ $TRAVIS_BRANCH == 'desktop' ] && [ $TRAVIS_OS_NAME == 'linux' ]; then
./scripts/linux.sh
fi

View File

@ -56,8 +56,19 @@
"addon.block_timeline.pluginname": "block_timeline", "addon.block_timeline.pluginname": "block_timeline",
"addon.block_timeline.sortbycourses": "block_timeline", "addon.block_timeline.sortbycourses": "block_timeline",
"addon.block_timeline.sortbydates": "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.calendar": "calendar",
"addon.calendar.calendarevents": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp",
"addon.calendar.calendarreminders": "local_moodlemobileapp",
"addon.calendar.defaultnotificationtime": "local_moodlemobileapp", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp",
"addon.calendar.errorloadevent": "local_moodlemobileapp", "addon.calendar.errorloadevent": "local_moodlemobileapp",
"addon.calendar.errorloadevents": "local_moodlemobileapp", "addon.calendar.errorloadevents": "local_moodlemobileapp",
@ -65,7 +76,8 @@
"addon.calendar.eventstarttime": "calendar", "addon.calendar.eventstarttime": "calendar",
"addon.calendar.gotoactivity": "calendar", "addon.calendar.gotoactivity": "calendar",
"addon.calendar.noevents": "local_moodlemobileapp", "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.typecategory": "calendar",
"addon.calendar.typeclose": "calendar", "addon.calendar.typeclose": "calendar",
"addon.calendar.typecourse": "calendar", "addon.calendar.typecourse": "calendar",
@ -133,6 +145,7 @@
"addon.coursecompletion.criteriarequiredany": "completion", "addon.coursecompletion.criteriarequiredany": "completion",
"addon.coursecompletion.inprogress": "completion", "addon.coursecompletion.inprogress": "completion",
"addon.coursecompletion.manualselfcompletion": "completion", "addon.coursecompletion.manualselfcompletion": "completion",
"addon.coursecompletion.nottracked": "completion",
"addon.coursecompletion.notyetstarted": "completion", "addon.coursecompletion.notyetstarted": "completion",
"addon.coursecompletion.pending": "completion", "addon.coursecompletion.pending": "completion",
"addon.coursecompletion.required": "moodle", "addon.coursecompletion.required": "moodle",
@ -214,6 +227,9 @@
"addon.messages.unabletomessage": "message", "addon.messages.unabletomessage": "message",
"addon.messages.unblockuser": "message", "addon.messages.unblockuser": "message",
"addon.messages.unblockuserconfirm": "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.userwouldliketocontactyou": "message",
"addon.messages.warningconversationmessagenotsent": "local_moodlemobileapp", "addon.messages.warningconversationmessagenotsent": "local_moodlemobileapp",
"addon.messages.warningmessagenotsent": "local_moodlemobileapp", "addon.messages.warningmessagenotsent": "local_moodlemobileapp",
@ -328,9 +344,12 @@
"addon.mod_assign_submission_comments.pluginname": "assignsubmission_comments", "addon.mod_assign_submission_comments.pluginname": "assignsubmission_comments",
"addon.mod_assign_submission_file.pluginname": "assignsubmission_file", "addon.mod_assign_submission_file.pluginname": "assignsubmission_file",
"addon.mod_assign_submission_onlinetext.pluginname": "assignsubmission_onlinetext", "addon.mod_assign_submission_onlinetext.pluginname": "assignsubmission_onlinetext",
"addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext",
"addon.mod_book.errorchapter": "book", "addon.mod_book.errorchapter": "book",
"addon.mod_book.modulenameplural": "book", "addon.mod_book.modulenameplural": "book",
"addon.mod_book.toc": "book",
"addon.mod_chat.beep": "chat", "addon.mod_chat.beep": "chat",
"addon.mod_chat.chatreport": "chat",
"addon.mod_chat.currentusers": "chat", "addon.mod_chat.currentusers": "chat",
"addon.mod_chat.enterchat": "chat", "addon.mod_chat.enterchat": "chat",
"addon.mod_chat.entermessage": "chat", "addon.mod_chat.entermessage": "chat",
@ -342,12 +361,16 @@
"addon.mod_chat.messagebeepsyou": "chat", "addon.mod_chat.messagebeepsyou": "chat",
"addon.mod_chat.messageenter": "chat", "addon.mod_chat.messageenter": "chat",
"addon.mod_chat.messageexit": "chat", "addon.mod_chat.messageexit": "chat",
"addon.mod_chat.messages": "chat",
"addon.mod_chat.modulenameplural": "chat", "addon.mod_chat.modulenameplural": "chat",
"addon.mod_chat.mustbeonlinetosendmessages": "local_moodlemobileapp", "addon.mod_chat.mustbeonlinetosendmessages": "local_moodlemobileapp",
"addon.mod_chat.nomessages": "chat", "addon.mod_chat.nomessages": "chat",
"addon.mod_chat.nosessionsfound": "local_moodlemobileapp",
"addon.mod_chat.send": "chat", "addon.mod_chat.send": "chat",
"addon.mod_chat.sessionstart": "chat", "addon.mod_chat.sessionstart": "chat",
"addon.mod_chat.showincompletesessions": "local_moodlemobileapp",
"addon.mod_chat.talk": "chat", "addon.mod_chat.talk": "chat",
"addon.mod_chat.viewreport": "chat",
"addon.mod_choice.cannotsubmit": "choice", "addon.mod_choice.cannotsubmit": "choice",
"addon.mod_choice.choiceoptions": "choice", "addon.mod_choice.choiceoptions": "choice",
"addon.mod_choice.errorgetchoice": "local_moodlemobileapp", "addon.mod_choice.errorgetchoice": "local_moodlemobileapp",
@ -602,6 +625,7 @@
"addon.mod_lti.modulenameplural": "lti", "addon.mod_lti.modulenameplural": "lti",
"addon.mod_page.errorwhileloadingthepage": "local_moodlemobileapp", "addon.mod_page.errorwhileloadingthepage": "local_moodlemobileapp",
"addon.mod_page.modulenameplural": "page", "addon.mod_page.modulenameplural": "page",
"addon.mod_quiz.answercolon": "qtype_numerical",
"addon.mod_quiz.attemptfirst": "quiz", "addon.mod_quiz.attemptfirst": "quiz",
"addon.mod_quiz.attemptlast": "quiz", "addon.mod_quiz.attemptlast": "quiz",
"addon.mod_quiz.attemptnumber": "quiz", "addon.mod_quiz.attemptnumber": "quiz",
@ -732,6 +756,7 @@
"addon.mod_scorm.scormstatusnotdownloaded": "local_moodlemobileapp", "addon.mod_scorm.scormstatusnotdownloaded": "local_moodlemobileapp",
"addon.mod_scorm.scormstatusoutdated": "local_moodlemobileapp", "addon.mod_scorm.scormstatusoutdated": "local_moodlemobileapp",
"addon.mod_scorm.suspended": "scorm", "addon.mod_scorm.suspended": "scorm",
"addon.mod_scorm.toc": "scorm",
"addon.mod_scorm.warningofflinedatadeleted": "local_moodlemobileapp", "addon.mod_scorm.warningofflinedatadeleted": "local_moodlemobileapp",
"addon.mod_scorm.warningsynconlineincomplete": "local_moodlemobileapp", "addon.mod_scorm.warningsynconlineincomplete": "local_moodlemobileapp",
"addon.mod_survey.cannotsubmitsurvey": "local_moodlemobileapp", "addon.mod_survey.cannotsubmitsurvey": "local_moodlemobileapp",
@ -1246,6 +1271,7 @@
"core.courses.enrolme": "local_moodlemobileapp", "core.courses.enrolme": "local_moodlemobileapp",
"core.courses.errorloadcategories": "local_moodlemobileapp", "core.courses.errorloadcategories": "local_moodlemobileapp",
"core.courses.errorloadcourses": "local_moodlemobileapp", "core.courses.errorloadcourses": "local_moodlemobileapp",
"core.courses.errorloadplugins": "local_moodlemobileapp",
"core.courses.errorsearching": "local_moodlemobileapp", "core.courses.errorsearching": "local_moodlemobileapp",
"core.courses.errorselfenrol": "local_moodlemobileapp", "core.courses.errorselfenrol": "local_moodlemobileapp",
"core.courses.filtermycourses": "local_moodlemobileapp", "core.courses.filtermycourses": "local_moodlemobileapp",
@ -1278,6 +1304,7 @@
"core.defaultvalue": "tool_usertours", "core.defaultvalue": "tool_usertours",
"core.delete": "moodle", "core.delete": "moodle",
"core.deletedoffline": "local_moodlemobileapp", "core.deletedoffline": "local_moodlemobileapp",
"core.deleteduser": "bulkusers",
"core.deleting": "local_moodlemobileapp", "core.deleting": "local_moodlemobileapp",
"core.description": "moodle", "core.description": "moodle",
"core.dfdaymonthyear": "local_moodlemobileapp", "core.dfdaymonthyear": "local_moodlemobileapp",
@ -1484,6 +1511,7 @@
"core.maxsizeandattachments": "moodle", "core.maxsizeandattachments": "moodle",
"core.min": "moodle", "core.min": "moodle",
"core.mins": "moodle", "core.mins": "moodle",
"core.misc": "admin",
"core.mod_assign": "assign/pluginname", "core.mod_assign": "assign/pluginname",
"core.mod_assignment": "assignment/pluginname", "core.mod_assignment": "assignment/pluginname",
"core.mod_book": "book/pluginname", "core.mod_book": "book/pluginname",
@ -1528,6 +1556,7 @@
"core.noresults": "moodle", "core.noresults": "moodle",
"core.notapplicable": "local_moodlemobileapp", "core.notapplicable": "local_moodlemobileapp",
"core.notice": "moodle", "core.notice": "moodle",
"core.notingroup": "moodle",
"core.notsent": "local_moodlemobileapp", "core.notsent": "local_moodlemobileapp",
"core.now": "moodle", "core.now": "moodle",
"core.numwords": "moodle", "core.numwords": "moodle",
@ -1567,11 +1596,20 @@
"core.question.questionno": "question", "core.question.questionno": "question",
"core.question.requiresgrading": "question", "core.question.requiresgrading": "question",
"core.quotausage": "moodle", "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.redirectingtosite": "local_moodlemobileapp",
"core.refresh": "moodle", "core.refresh": "moodle",
"core.remove": "moodle", "core.remove": "moodle",
"core.required": "moodle", "core.required": "moodle",
"core.requireduserdatamissing": "local_moodlemobileapp", "core.requireduserdatamissing": "local_moodlemobileapp",
"core.resourcedisplayopen": "moodle",
"core.resources": "moodle", "core.resources": "moodle",
"core.restore": "moodle", "core.restore": "moodle",
"core.retry": "local_moodlemobileapp", "core.retry": "local_moodlemobileapp",
@ -1626,6 +1664,7 @@
"core.settings.navigatoruseragent": "local_moodlemobileapp", "core.settings.navigatoruseragent": "local_moodlemobileapp",
"core.settings.networkstatus": "local_moodlemobileapp", "core.settings.networkstatus": "local_moodlemobileapp",
"core.settings.privacypolicy": "local_moodlemobileapp", "core.settings.privacypolicy": "local_moodlemobileapp",
"core.settings.pushid": "local_moodlemobileapp",
"core.settings.reportinbackground": "local_moodlemobileapp", "core.settings.reportinbackground": "local_moodlemobileapp",
"core.settings.settings": "moodle", "core.settings.settings": "moodle",
"core.settings.showdownloadoptions": "local_moodlemobileapp", "core.settings.showdownloadoptions": "local_moodlemobileapp",

34
scripts/linux.sh 100755
View File

@ -0,0 +1,34 @@
#!/bin/bash
#
# Script for generating the Desktop builds
#
sudo apt-get install -y libnss3-dev
npm install -g electron-builder electron
electron-builder install-app-deps
jq -s '.[0] + {"name": "moodledesktop"}' package.json > package_new.json
mv package_new.json package.json
rm -Rf desktop/dist
npm run desktop.dist -- -l --x64 --ia32
if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] ; then
git clone -q https://$GIT_TOKEN@github.com/moodlemobile/bma-apps-builds.git ../apps
mv desktop/dist/*.AppImage ../apps
cd ../apps
chmod +x *.AppImage
mv *i386.AppImage linux-ia32.AppImage
mv Moodle*.AppImage linux-x64.AppImage
ls
git add .
git commit -m "Linux desktop versions from Travis build $TRAVIS_BUILD_NUMBER"
git push
fi

View File

@ -19,7 +19,7 @@ import { CoreLoginHelperProvider } from '@core/login/providers/helper';
import { AddonBadgesProvider } from './badges'; import { AddonBadgesProvider } from './badges';
/** /**
* Handler to treat links to user participants page. * Handler to treat links to user badges page.
*/ */
@Injectable() @Injectable()
export class AddonBadgesMyBadgesLinkHandler extends CoreContentLinksHandlerBase { export class AddonBadgesMyBadgesLinkHandler extends CoreContentLinksHandlerBase {

View File

@ -0,0 +1,51 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
import { CoreUserDelegate } from '@core/user/providers/user-delegate';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { AddonBlogProvider } from './providers/blog';
import { AddonBlogMainMenuHandler } from './providers/mainmenu-handler';
import { AddonBlogUserHandler } from './providers/user-handler';
import { AddonBlogCourseOptionHandler } from './providers/course-option-handler';
import { AddonBlogComponentsModule } from './components/components.module';
import { AddonBlogIndexLinkHandler } from './providers/index-link-handler';
@NgModule({
declarations: [
],
imports: [
AddonBlogComponentsModule
],
providers: [
AddonBlogProvider,
AddonBlogMainMenuHandler,
AddonBlogUserHandler,
AddonBlogCourseOptionHandler,
AddonBlogIndexLinkHandler
]
})
export class AddonBlogModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, menuHandler: AddonBlogMainMenuHandler,
userHandler: AddonBlogUserHandler, userDelegate: CoreUserDelegate,
courseOptionHandler: AddonBlogCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate,
linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate) {
mainMenuDelegate.registerHandler(menuHandler);
userDelegate.registerHandler(userHandler);
courseOptionsDelegate.registerHandler(courseOptionHandler);
contentLinksDelegate.registerHandler(linkHandler);
}
}

View File

@ -0,0 +1,47 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
import { AddonBlogEntriesComponent } from './entries/entries';
@NgModule({
declarations: [
AddonBlogEntriesComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreCommentsComponentsModule
],
providers: [
],
exports: [
AddonBlogEntriesComponent
],
entryComponents: [
AddonBlogEntriesComponent
]
})
export class AddonBlogComponentsModule {}

View File

@ -0,0 +1,49 @@
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<div class="safe-padding-horizontal">
<ion-item *ngIf="showMyIssuesToggle">
<ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label>
<ion-toggle [(ngModel)]="onlyMyEntries"></ion-toggle>
</ion-item>
</div>
<core-empty-box *ngIf="entries && entries.length == 0" icon="fa-newspaper-o" [message]="'addon.blog.noentriesyet' | translate"></core-empty-box>
<ng-container *ngFor="let entry of entries">
<ion-card *ngIf="!onlyMyEntries || entry.userid == currentUserId">
<ion-item text-wrap>
<ion-avatar core-user-avatar [user]="entry.user" item-start [courseId]="entry.courseid"></ion-avatar>
<h2>
<core-format-text [text]="entry.subject"></core-format-text>
<ion-note float-end padding-left text-end>
{{ 'addon.blog.' + entry.publishTranslated | translate}}
</ion-note>
</h2>
<p>
<ion-note float-end padding-left text-end>
{{entry.created | coreDateDayOrTime}}
</ion-note>
{{entry.user && entry.user.fullname}}
</p>
</ion-item>
<ion-card-content>
<core-format-text [text]="entry.summary" [component]="this.component" [componentId]="entry.id"></core-format-text>
<ion-item>
<core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid" contextLevel="user"></core-comments>
</ion-item>
<core-file *ngFor="let file of entry.attachmentfiles" [file]="file" [component]="this.component" [componentId]="entry.id"></core-file>
<a ion-item *ngIf="entry.uniquehash" [href]="entry.uniquehash" core-link>{{ 'addon.blog.linktooriginalentry' | translate }}</a>
</ion-card-content>
<ion-row text-center>
<ion-col *ngIf="entry.lastmodified > entry.created">
<ion-note>
<ion-icon name="time"></ion-icon> {{entry.lastmodified | coreTimeAgo}}
</ion-note>
</ion-col>
</ion-row>
</ion-card>
</ng-container>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading>
</core-loading>
</ion-content>

View File

@ -0,0 +1,176 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { Content } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonBlogProvider } from '../../providers/blog';
/**
* Component that displays the blog entries.
*/
@Component({
selector: 'addon-blog-entries',
templateUrl: 'addon-blog-entries.html',
})
export class AddonBlogEntriesComponent implements OnInit {
@Input() userId?: number;
@Input() courseId?: number;
@Input() cmId?: number;
@Input() entryId?: number;
@Input() groupId?: number;
@Input() tagId?: number;
protected filter = {};
protected pageLoaded = 0;
@ViewChild(Content) content: Content;
loaded = false;
canLoadMore = false;
loadMoreError = false;
entries = [];
currentUserId: number;
showMyIssuesToggle = false;
onlyMyEntries = false;
component = AddonBlogProvider.COMPONENT;
constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider,
protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider) {
this.currentUserId = sitesProvider.getCurrentSiteUserId();
}
/**
* Component being initialized.
*/
ngOnInit(): void {
if (this.userId) {
this.filter['userid'] = this.userId;
}
if (this.courseId) {
this.filter['courseid'] = this.courseId;
}
if (this.cmId) {
this.filter['cmid'] = this.cmId;
}
if (this.entryId) {
this.filter['entryid'] = this.entryId;
}
if (this.groupId) {
this.filter['groupid'] = this.groupId;
}
if (this.tagId) {
this.filter['tagid'] = this.tagId;
}
this.fetchEntries().then(() => {
this.blogProvider.logView(this.filter).catch(() => {
// Ignore errors.
});
});
}
/**
* Fetch blog entries.
*
* @param {boolean} [refresh] Empty events array first.
* @return {Promise<any>} Promise with the entries.
*/
private fetchEntries(refresh: boolean = false): Promise<any> {
this.loadMoreError = false;
if (refresh) {
this.pageLoaded = 0;
}
return this.blogProvider.getEntries(this.filter, this.pageLoaded).then((result) => {
const promises = result.entries.map((entry) => {
switch (entry.publishstate) {
case 'draft':
entry.publishTranslated = 'publishtonoone';
break;
case 'site':
entry.publishTranslated = 'publishtosite';
break;
case 'public':
entry.publishTranslated = 'publishtoworld';
break;
default:
entry.publishTranslated = 'privacy:unknown';
break;
}
return this.userProvider.getProfile(entry.userid, entry.courseid, true).then((user) => {
entry.user = user;
}).catch(() => {
// Ignore errors.
});
});
if (refresh) {
this.showMyIssuesToggle = false;
this.entries = result.entries;
} else {
this.entries = this.entries.concat(result.entries);
}
this.canLoadMore = result.totalentries > this.entries.length;
this.pageLoaded++;
this.showMyIssuesToggle = !this.userId;
return Promise.all(promises);
}).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'addon.blog.errorloadentries', true);
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
}).finally(() => {
this.loaded = true;
});
}
/**
* Function to load more entries.
*
* @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading.
* @return {Promise<any>} Resolved when done.
*/
loadMore(infiniteComplete?: any): Promise<any> {
return this.fetchEntries().finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Refresh blog entries on PTR.
*
* @param {any} refresher Refresher instance.
*/
refresh(refresher?: any): void {
this.blogProvider.invalidateEntries(this.filter).finally(() => {
this.fetchEntries(true).finally(() => {
if (refresher) {
refresher.complete();
}
});
});
}
}

View File

@ -0,0 +1,12 @@
{
"blog": "Blog",
"blogentries": "Blog entries",
"errorloadentries": "Error loading blog entries.",
"linktooriginalentry": "Link to original blog entry",
"noentriesyet": "No visible entries here",
"publishtonoone": "Yourself (draft)",
"publishtosite": "Anyone on this site",
"publishtoworld": "Anyone in the world",
"showonlyyourentries": "Show only your entries",
"siteblogheading": "Site blog"
}

View File

@ -0,0 +1,7 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ title | translate }}</ion-title>
<ion-buttons end></ion-buttons>
</ion-navbar>
</ion-header>
<addon-blog-entries class="core-avoid-header" [courseId]="courseId" [userId]="userId" [cmId]="cmId" [entryId]="entryId" [groupId]="groupId" [tagId]="tagId"></addon-blog-entries>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonBlogEntriesPage } from './entries';
import { AddonBlogComponentsModule } from '../../components/components.module';
@NgModule({
declarations: [
AddonBlogEntriesPage,
],
imports: [
CoreDirectivesModule,
AddonBlogComponentsModule,
IonicPageModule.forChild(AddonBlogEntriesPage),
TranslateModule.forChild()
],
})
export class AddonBlogEntriesPageModule {}

View File

@ -0,0 +1,49 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
/**
* Page that displays the list of blog entries.
*/
@IonicPage({ segment: 'addon-blog-entries' })
@Component({
selector: 'page-addon-blog-entries',
templateUrl: 'entries.html',
})
export class AddonBlogEntriesPage {
userId: number;
courseId: number;
cmId: number;
entryId: number;
groupId: number;
tagId: number;
title: string;
constructor(params: NavParams) {
this.userId = params.get('userId');
this.courseId = params.get('courseId');
this.cmId = params.get('cmId');
this.entryId = params.get('entryId');
this.groupId = params.get('groupId');
this.tagId = params.get('tagId');
if (!this.userId && !this.courseId && !this.cmId && !this.entryId && !this.groupId && !this.tagId) {
this.title = 'addon.blog.siteblogheading';
} else {
this.title = 'addon.blog.blogentries';
}
}
}

View File

@ -0,0 +1,113 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
/**
* Service to handle blog entries.
*/
@Injectable()
export class AddonBlogProvider {
static ENTRIES_PER_PAGE = 10;
static COMPONENT = 'blog';
protected ROOT_CACHE_KEY = 'addonBlog:';
protected logger;
constructor(logger: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected utils: CoreUtilsProvider) {
this.logger = logger.getInstance('AddonBlogProvider');
}
/**
* Returns whether or not the blog plugin is enabled for a certain site.
*
* This method is called quite often and thus should only perform a quick
* check, we should not be calling WS from here.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false or rejected otherwise.
*/
isPluginEnabled(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.wsAvailable('core_blog_get_entries') &&
site.canUseAdvancedFeature('enableblogs');
});
}
/**
* Get the cache key for the blog entries.
*
* @param {any} [filter] Filter to apply on search.
* @return {string} Cache key.
*/
getEntriesCacheKey(filter: any = {}): string {
return this.ROOT_CACHE_KEY + this.utils.sortAndStringify(filter);
}
/**
* Get blog entries.
*
* @param {any} [filter] Filter to apply on search.
* @param {any} [page=0] Page of the blog entries to fetch.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise to be resolved when the entries are retrieved.
*/
getEntries(filter: any = {}, page: number = 0, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value'),
page: page,
perpage: AddonBlogProvider.ENTRIES_PER_PAGE
};
const preSets = {
cacheKey: this.getEntriesCacheKey(filter)
};
return site.read('core_blog_get_entries', data, preSets);
});
}
/**
* Invalidate blog entries WS call.
*
* @param {any} [filter] Filter to apply on search
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when data is invalidated.
*/
invalidateEntries(filter: any = {}, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getEntriesCacheKey(filter));
});
}
/**
* Trigger the blog_entries_viewed event.
*
* @param {any} [filter] Filter to apply on search.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise to be resolved when done.
*/
logView(filter: any = {}, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value')
};
return site.write('core_blog_view_entries', data);
});
}
}

View File

@ -0,0 +1,120 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { AddonBlogEntriesComponent } from '../components/entries/entries';
import { AddonBlogProvider } from './blog';
/**
* Course nav handler.
*/
@Injectable()
export class AddonBlogCourseOptionHandler implements CoreCourseOptionsHandler {
name = 'AddonBlog';
priority = 100;
constructor(protected coursesProvider: CoreCoursesProvider, protected blogProvider: AddonBlogProvider,
protected courseHelper: CoreCourseHelperProvider, protected courseProvider: CoreCourseProvider,
protected sitesProvider: CoreSitesProvider, protected filepoolProvider: CoreFilepoolProvider) {}
/**
* Should invalidate the data to determine if the handler is enabled for a certain course.
*
* @param {number} courseId The course ID.
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return {Promise<any>} Promise resolved when done.
*/
invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise<any> {
return this.courseProvider.invalidateCourseBlocks(courseId);
}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.blogProvider.isPluginEnabled();
}
/**
* Whether or not the handler is enabled for a certain course.
*
* @param {number} courseId The course ID.
* @param {any} accessData Access type and data. Default, guest, ...
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
return this.courseHelper.hasABlockNamed(courseId, 'blog_menu').then((enabled) => {
if (enabled && navOptions && typeof navOptions.blogs != 'undefined') {
return navOptions.blogs;
}
return enabled;
});
}
/**
* Returns the data needed to render the handler.
*
* @param {Injector} injector Injector.
* @param {number} courseId The course ID.
* @return {CoreCourseOptionsHandlerData|Promise<CoreCourseOptionsHandlerData>} Data or promise resolved with the data.
*/
getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> {
return {
title: 'addon.blog.blog',
class: 'addon-blog-handler',
component: AddonBlogEntriesComponent
};
}
/**
* Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
*
* @param {any} course The course.
* @return {Promise<any>} Promise resolved when done.
*/
prefetch(course: any): Promise<any> {
const siteId = this.sitesProvider.getCurrentSiteId();
return this.blogProvider.getEntries({courseid: course.id}).then((result) => {
return result.entries.map((entry) => {
let files = [];
if (entry.attachmentfiles && entry.attachmentfiles.length) {
files = entry.attachmentfiles;
}
if (entry.summaryfiles && entry.summaryfiles.length) {
files = files.concat(entry.summaryfiles);
}
if (files.length > 0) {
return this.filepoolProvider.addFilesToQueue(siteId, files, entry.module, entry.id);
}
return Promise.resolve();
});
});
}
}

View File

@ -0,0 +1,76 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
import { AddonBlogProvider } from './blog';
/**
* Handler to treat links to blog page.
*/
@Injectable()
export class AddonBlogIndexLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonBlogIndexLinkHandler';
featureName = 'CoreUserDelegate_AddonBlog';
pattern = /\/blog\/index\.php/;
constructor(private blogProvider: AddonBlogProvider, private loginHelper: CoreLoginHelperProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
const pageParams: any = {};
params.userid ? pageParams['userId'] = parseInt(params.userid, 10) : null;
params.modid ? pageParams['cmId'] = parseInt(params.modid, 10) : null;
params.courseid ? pageParams['courseId'] = parseInt(params.courseid, 10) : null;
params.entryid ? pageParams['entryId'] = parseInt(params.entryid, 10) : null;
params.groupid ? pageParams['groupId'] = parseInt(params.groupid, 10) : null;
params.tagid ? pageParams['tagId'] = parseInt(params.tagid, 10) : null;
return [{
action: (siteId, navCtrl?): void => {
// Always use redirect to make it the new history root (to avoid "loops" in history).
this.loginHelper.redirect('AddonBlogEntriesPage', pageParams, siteId);
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
return this.blogProvider.isPluginEnabled(siteId);
}
}

View File

@ -0,0 +1,51 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { AddonBlogProvider } from './blog';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate';
/**
* Handler to inject an option into main menu.
*/
@Injectable()
export class AddonBlogMainMenuHandler implements CoreMainMenuHandler {
name = 'AddonBlog';
priority = 450;
constructor(private blogProvider: AddonBlogProvider) { }
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.blogProvider.isPluginEnabled();
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreMainMenuHandlerData {
return {
icon: 'fa-newspaper-o',
title: 'addon.blog.siteblogheading',
page: 'AddonBlogEntriesPage',
class: 'addon-blog-handler'
};
}
}

View File

@ -0,0 +1,71 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { AddonBlogProvider } from './blog';
/**
* Profile item handler.
*/
@Injectable()
export class AddonBlogUserHandler implements CoreUserProfileHandler {
name = 'AddonBlog:blogs';
priority = 300;
type = CoreUserDelegate.TYPE_NEW_PAGE;
constructor(protected linkHelper: CoreContentLinksHelperProvider, protected blogProvider: AddonBlogProvider) {
}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.blogProvider.isPluginEnabled();
}
/**
* Check if handler is enabled for this user in this context.
*
* @param {any} user User to check.
* @param {number} courseId Course ID.
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return {boolean|Promise<boolean>} Promise resolved with true if enabled, resolved with false otherwise.
*/
isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
return true;
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreUserProfileHandlerData} Data needed to render the handler.
*/
getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData {
return {
icon: 'fa-newspaper-o',
title: 'addon.blog.blogentries',
class: 'addon-blog-handler',
action: (event, navCtrl, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
// Always use redirect to make it the new history root (to avoid "loops" in history).
this.linkHelper.goInSite(navCtrl, 'AddonBlogEntriesPage', { userId: user.id, courseId: courseId });
}
};
}
}

View File

@ -71,7 +71,7 @@ export class AddonCalendarModule {
newName: AddonCalendarProvider.EVENTS_TABLE, newName: AddonCalendarProvider.EVENTS_TABLE,
filterFields: ['id', 'name', 'description', 'format', 'eventtype', 'courseid', 'timestart', 'timeduration', filterFields: ['id', 'name', 'description', 'format', 'eventtype', 'courseid', 'timestart', 'timeduration',
'categoryid', 'groupid', 'userid', 'instance', 'modulename', 'timemodified', 'repeatid', 'visible', 'uuid', 'categoryid', 'groupid', 'userid', 'instance', 'modulename', 'timemodified', 'repeatid', 'visible', 'uuid',
'sequence', 'subscriptionid', 'notificationtime'] 'sequence', 'subscriptionid']
}); });
// Migrate the component name. // Migrate the component name.

View File

@ -1,6 +1,7 @@
{ {
"calendar": "Calendar", "calendar": "Calendar",
"calendarevents": "Calendar events", "calendarevents": "Calendar events",
"calendarreminders": "Calendar reminders",
"defaultnotificationtime": "Default notification time", "defaultnotificationtime": "Default notification time",
"errorloadevent": "Error loading event.", "errorloadevent": "Error loading event.",
"errorloadevents": "Error loading events.", "errorloadevents": "Error loading events.",
@ -8,7 +9,8 @@
"eventstarttime": "Start time", "eventstarttime": "Start time",
"gotoactivity": "Go to activity", "gotoactivity": "Go to activity",
"noevents": "There are no events", "noevents": "There are no events",
"notifications": "Notifications", "reminders": "Reminders",
"setnewreminder": "Set a new reminder",
"typeclose": "Close event", "typeclose": "Close event",
"typecourse": "Course event", "typecourse": "Course event",
"typecategory": "Category event", "typecategory": "Category event",

View File

@ -10,10 +10,10 @@
<core-loading [hideUntil]="eventLoaded"> <core-loading [hideUntil]="eventLoaded">
<ion-card> <ion-card>
<ion-card-content *ngIf="event"> <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-icon *ngIf="event.icon && !event.moduleIcon" [name]="event.icon" item-start></core-icon>
<core-format-text [text]="event.name"></core-format-text> <h2><core-format-text [text]="event.name"></core-format-text></h2>
</ion-card-title> </ion-item>
<ion-item text-wrap> <ion-item text-wrap>
<h2>{{ 'addon.calendar.eventstarttime' | translate}}</h2> <h2>{{ 'addon.calendar.eventstarttime' | translate}}</h2>
<p>{{ event.timestart * 1000 | coreFormatDate }}</p> <p>{{ event.timestart * 1000 | coreFormatDate }}</p>
@ -52,21 +52,29 @@
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>
<ion-card list *ngIf="notificationsEnabled && event.timestart - 600 > currentTime"> <ion-card list *ngIf="notificationsEnabled">
<ion-item> <ion-item>
<ion-label>{{ 'addon.calendar.notifications' | translate }}</ion-label> <h2>{{ 'addon.calendar.reminders' | translate }}</h2>
<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>
</ion-item> </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> </ion-card>
</core-loading> </core-loading>
</ion-content> </ion-content>

View File

@ -39,8 +39,10 @@ export class AddonCalendarEventPage {
protected eventId; protected eventId;
protected siteHomeId: number; protected siteHomeId: number;
eventLoaded: boolean; eventLoaded: boolean;
notificationTime: number; notificationFormat: string;
defaultTimeReadable: string; notificationMin: string;
notificationMax: string;
notificationTimeText: string;
event: any = {}; event: any = {};
title: string; title: string;
courseName: string; courseName: string;
@ -50,6 +52,7 @@ export class AddonCalendarEventPage {
categoryPath = ''; categoryPath = '';
currentTime: number; currentTime: number;
defaultTime: number; defaultTime: number;
reminders: any[];
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider,
@ -61,21 +64,17 @@ export class AddonCalendarEventPage {
this.notificationsEnabled = localNotificationsProvider.isAvailable(); this.notificationsEnabled = localNotificationsProvider.isAvailable();
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
if (this.notificationsEnabled) { if (this.notificationsEnabled) {
this.calendarProvider.getEventNotificationTimeOption(this.eventId).then((notificationTime) => { this.calendarProvider.getEventReminders(this.eventId).then((reminders) => {
this.notificationTime = notificationTime; this.reminders = reminders;
this.loadNotificationTime();
}); });
this.calendarProvider.getDefaultNotificationTime().then((defaultTime) => { this.calendarProvider.getDefaultNotificationTime().then((defaultTime) => {
this.defaultTime = defaultTime * 60; 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. * Fetches the event and updates the view.
* *
@ -117,7 +110,9 @@ export class AddonCalendarEventPage {
this.event = event; this.event = event;
this.currentTime = this.timeUtils.timestamp(); 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. // Reset some of the calculated data.
this.categoryPath = ''; this.categoryPath = '';
@ -187,16 +182,50 @@ 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 { addNotificationTime(e: Event): void {
if (typeof this.notificationTime != 'undefined') { e.preventDefault();
if (this.notificationTime > 0 && this.event.timestart - this.notificationTime * 60 < this.currentTime) { e.stopPropagation();
this.notificationTime = 0;
} else if (this.notificationTime < 0 && this.event.timestart - this.defaultTime < this.currentTime) { if (this.notificationTimeText && this.event && this.event.id) {
this.notificationTime = 0; 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;
});
});
} }
/** /**

View File

@ -13,9 +13,8 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
@ -23,6 +22,8 @@ import { CoreGroupsProvider } from '@providers/groups';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreConfigProvider } from '@providers/config'; import { CoreConfigProvider } from '@providers/config';
import { ILocalNotification } from '@ionic-native/local-notifications';
import { SQLiteDB } from '@classes/sqlitedb';
/** /**
* Service to handle calendar events. * Service to handle calendar events.
@ -37,8 +38,12 @@ export class AddonCalendarProvider {
protected ROOT_CACHE_KEY = 'mmaCalendar:'; protected ROOT_CACHE_KEY = 'mmaCalendar:';
// Variables for database. // Variables for database.
static EVENTS_TABLE = 'addon_calendar_events'; static EVENTS_TABLE = 'addon_calendar_events_2';
protected tablesSchema = [ static REMINDERS_TABLE = 'addon_calendar_reminders';
protected siteSchema: CoreSiteSchema = {
name: 'AddonCalendarProvider',
version: 2,
tables: [
{ {
name: AddonCalendarProvider.EVENTS_TABLE, name: AddonCalendarProvider.EVENTS_TABLE,
columns: [ columns: [
@ -47,10 +52,6 @@ export class AddonCalendarProvider {
type: 'INTEGER', type: 'INTEGER',
primaryKey: true primaryKey: true
}, },
{
name: 'notificationtime',
type: 'INTEGER'
},
{ {
name: 'name', name: 'name',
type: 'TEXT', type: 'TEXT',
@ -125,46 +126,131 @@ export class AddonCalendarProvider {
type: 'INTEGER' 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; protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider,
private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider) {
private translate: TranslateService) {
this.logger = logger.getInstance('AddonCalendarProvider'); this.logger = logger.getInstance('AddonCalendarProvider');
this.sitesProvider.createTablesFromSchema(this.tablesSchema); this.sitesProvider.registerSiteSchema(this.siteSchema);
} }
/** /**
* Removes expired events from local DB. * Removes expired events from local DB.
* *
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @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) => { return this.sitesProvider.getSite(siteId).then((site) => {
let promise; return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?',
// Cancel expired events notifications first.
if (this.localNotificationsProvider.isAvailable()) {
promise = site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart < ?',
[this.timeUtils.timestamp()]).then((events) => { [this.timeUtils.timestamp()]).then((events) => {
events.forEach((event) => { return Promise.all(events.map((event) => {
return this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, site.getId()); return this.deleteEvent(event.id, siteId);
}));
}); });
}).catch(() => {
// Ignore errors.
}); });
} else {
promise = Promise.resolve();
} }
return promise.then(() => { /**
return site.getDb().deleteRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart < ?', * Delete event cancelling all the reminders and notifications.
[this.timeUtils.timestamp()]); *
* @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();
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. * @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> { addEventReminder(event: any, time: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); return this.sitesProvider.getSite(siteId).then((site) => {
const reminder = {
eventid: event.id,
time: time
};
return this.getEventNotificationTimeOption(id, siteId).then((time: number) => { return site.getDb().insertRecord(AddonCalendarProvider.REMINDERS_TABLE, reminder).then((reminderId) => {
if (time == -1) { return this.scheduleEventNotification(event, reminderId, time, site.getId());
return this.getDefaultNotificationTime(siteId); });
}
return time;
}); });
} }
/** /**
* 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 {number} id Event ID.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @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> { getEventReminders(id: number, siteId?: string): Promise<any> {
return this.getEventFromLocalDb(id, siteId).then((e) => { return this.sitesProvider.getSite(siteId).then((site) => {
return e.notificationtime || -1; return site.getDb().getRecords(AddonCalendarProvider.REMINDERS_TABLE, {eventid: id}, 'time ASC');
}).catch(() => {
return -1;
}); });
} }
@ -507,38 +610,48 @@ export class AddonCalendarProvider {
* @param {string} [siteId] Site ID the event belongs to. If not defined, use current site. * @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. * @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()) { if (this.localNotificationsProvider.isAvailable()) {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (time === 0) { if (time === 0) {
// Cancel if it was scheduled. // 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. let promise;
const promise = time == -1 ? this.getDefaultNotificationTime(siteId) : Promise.resolve(time); 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) => { return promise.then((time) => {
const timeEnd = (event.timestart + event.timeduration) * 1000; time = time * 1000;
if (timeEnd <= new Date().getTime()) {
// The event has finished already, don't schedule it. if (time <= new Date().getTime()) {
return Promise.resolve(); // 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), const notification: ILocalNotification = {
notification = { id: reminderId,
id: event.id,
title: event.name, title: event.name,
text: this.timeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true), text: this.timeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true),
at: dateTriggered, trigger: {
channelParams: { at: new Date(time)
channelID: 'notifications',
channelName: this.translate.instant('addon.notifications.notifications'),
importance: 4 // IMPORTANCE_HIGH
}, },
data: { data: {
eventid: event.id, eventid: event.id,
reminderid: reminderId,
siteid: siteId siteid: siteId
} }
}; };
@ -561,18 +674,27 @@ export class AddonCalendarProvider {
* @return {Promise<any[]>} Promise resolved when all the notifications have been scheduled. * @return {Promise<any[]>} Promise resolved when all the notifications have been scheduled.
*/ */
scheduleEventsNotifications(events: any[], siteId?: string): Promise<any[]> { scheduleEventsNotifications(events: any[], siteId?: string): Promise<any[]> {
const promises = [];
if (this.localNotificationsProvider.isAvailable()) { if (this.localNotificationsProvider.isAvailable()) {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
events.forEach((e) => {
promises.push(this.getEventNotificationTime(e.id, siteId).then((time) => { return Promise.all(events.map((event) => {
return this.scheduleEventNotification(e, time, siteId); 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 Promise.all(promises); 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.resolve([]);
} }
/** /**
@ -598,31 +720,18 @@ export class AddonCalendarProvider {
* @return {Promise<any>} Promise resolved when stored. * @return {Promise<any>} Promise resolved when stored.
*/ */
storeEventInLocalDb(event: any, siteId?: string): Promise<any> { storeEventInLocalDb(event: any, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, event);
});
}
/**
* Store events in local DB.
*
* @param {any[]} events Events to store.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved when the events are stored.
*/
protected storeEventsInLocalDB(events: any[], siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId(); siteId = site.getId();
const promises = [], // If event does not exist on the DB, schedule the reminder.
db = site.getDb(); return this.getEventFromLocalDb(event.id, site.id).catch(() => {
// Event does not exist. Check if any reminder exists first.
events.forEach((event) => { return this.getEventReminders(event.id, siteId).then((reminders) => {
// Don't override event notification time if the user configured it. if (reminders.length == 0) {
promises.push(this.getEventFromLocalDb(event.id, siteId).catch(() => { this.addEventReminder(event, -1, siteId);
// Event not stored, return empty object. }
return {}; });
}).then((e) => { }).then(() => {
const eventRecord = { const eventRecord = {
id: event.id, id: event.id,
name: event.name, name: event.name,
@ -642,37 +751,29 @@ export class AddonCalendarProvider {
visible: event.visible, visible: event.visible,
uuid: event.uuid, uuid: event.uuid,
sequence: event.sequence, sequence: event.sequence,
subscriptionid: event.subscriptionid, subscriptionid: event.subscriptionid
notificationtime: e.notificationtime || -1
}; };
return db.insertRecord(AddonCalendarProvider.EVENTS_TABLE, eventRecord); return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, eventRecord);
}));
}); });
return Promise.all(promises);
}); });
} }
/** /**
* Updates an event notification time and schedule a new notification. * Store events in local DB.
* *
* @param {any} event Event to update its notification time. * @param {any[]} events Events to store.
* @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. * @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. * @return {Promise<any[]>} Promise resolved when the events are stored.
*/ */
updateNotificationTime(event: any, time: number, siteId?: string): Promise<void> { protected storeEventsInLocalDB(events: any[], siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
if (!this.sitesProvider.isLoggedIn()) { siteId = site.getId();
// 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}) return Promise.all(events.map((event) => {
.then(() => { // If event does not exist on the DB, schedule the reminder.
return this.scheduleEventNotification(event, time); return this.storeEventInLocalDb(event, siteId);
}); }));
}); });
} }
} }

View File

@ -3,7 +3,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="completionLoaded"> <core-loading [hideUntil]="completionLoaded">
<ion-card *ngIf="completion"> <ion-card *ngIf="completion && tracked">
<ion-item text-wrap> <ion-item text-wrap>
<h2>{{ 'addon.coursecompletion.status' | translate }}</h2> <h2>{{ 'addon.coursecompletion.status' | translate }}</h2>
<p>{{ completion.statusText | translate }}</p> <p>{{ completion.statusText | translate }}</p>
@ -14,7 +14,7 @@
<p *ngIf="completion.aggregation === 2">{{ 'addon.coursecompletion.criteriarequiredany' | translate }}</p> <p *ngIf="completion.aggregation === 2">{{ 'addon.coursecompletion.criteriarequiredany' | translate }}</p>
</ion-item> </ion-item>
</ion-card> </ion-card>
<ion-card *ngIf="completion"> <ion-card *ngIf="completion && tracked">
<ion-item-divider>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</ion-item-divider> <ion-item-divider>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</ion-item-divider>
<ion-item class="hidden-tablet" text-wrap *ngFor="let criteria of completion.completions"> <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> <h2><core-format-text clean="true" [text]="criteria.details.criteria"></core-format-text></h2>
@ -41,11 +41,16 @@
</ion-row> </ion-row>
</ion-item> </ion-item>
</ion-card> </ion-card>
<ion-card *ngIf="showSelfComplete"> <ion-card *ngIf="showSelfComplete && tracked">
<ion-item-divider>{{ 'addon.coursecompletion.manualselfcompletion' | translate }}</ion-item-divider> <ion-item-divider>{{ 'addon.coursecompletion.manualselfcompletion' | translate }}</ion-item-divider>
<ion-item> <ion-item>
<button ion-button block (click)="completeCourse()">{{ 'addon.coursecompletion.completecourse' | translate }}</button> <button ion-button block (click)="completeCourse()">{{ 'addon.coursecompletion.completecourse' | translate }}</button>
</ion-item> </ion-item>
</ion-card> </ion-card>
<div *ngIf="!tracked" class="core-warning-card" icon-start>
<ion-icon name="warning"></ion-icon>
{{ 'addon.coursecompletion.nottracked' | translate }}
</div>
</core-loading> </core-loading>
</ion-content> </ion-content>

View File

@ -31,6 +31,7 @@ export class AddonCourseCompletionReportComponent implements OnInit {
completionLoaded = false; completionLoaded = false;
completion: any; completion: any;
showSelfComplete: boolean; showSelfComplete: boolean;
tracked = true; // Whether completion is tracked.
constructor( constructor(
private sitesProvider: CoreSitesProvider, private sitesProvider: CoreSitesProvider,
@ -62,8 +63,14 @@ export class AddonCourseCompletionReportComponent implements OnInit {
this.completion = completion; this.completion = completion;
this.showSelfComplete = this.courseCompletionProvider.canMarkSelfCompleted(this.userId, completion); this.showSelfComplete = this.courseCompletionProvider.canMarkSelfCompleted(this.userId, completion);
}).catch((message) => { this.tracked = true;
this.domUtils.showErrorModalDefault(message, 'addon.coursecompletion.couldnotloadreport', 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);
}
}); });
} }

View File

@ -12,6 +12,7 @@
"criteriarequiredany": "Any criteria below are required", "criteriarequiredany": "Any criteria below are required",
"inprogress": "In progress", "inprogress": "In progress",
"manualselfcompletion": "Manual self completion", "manualselfcompletion": "Manual self completion",
"nottracked": "You are currently not being tracked by completion in this course",
"notyetstarted": "Not yet started", "notyetstarted": "Not yet started",
"pending": "Pending", "pending": "Pending",
"required": "Required", "required": "Required",

View File

@ -62,7 +62,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
// Update discussions when new message is received. // Update discussions when new message is received.
this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => { this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => {
if (data.userId) { if (data.userId && this.discussions) {
const discussion = this.discussions.find((disc) => { const discussion = this.discussions.find((disc) => {
return disc.message.user == data.userId; return disc.message.user == data.userId;
}); });
@ -82,7 +82,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
// Update discussions when a message is read. // Update discussions when a message is read.
this.readChangedObserver = eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => { this.readChangedObserver = eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => {
if (data.userId) { if (data.userId && this.discussions) {
const discussion = this.discussions.find((disc) => { const discussion = this.discussions.find((disc) => {
return disc.message.user == data.userId; return disc.message.user == data.userId;
}); });
@ -92,8 +92,8 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
discussion.unread = false; discussion.unread = false;
// Conversations changed, invalidate them and refresh unread counts. // Conversations changed, invalidate them and refresh unread counts.
this.messagesProvider.invalidateConversations(); this.messagesProvider.invalidateConversations(this.siteId);
this.messagesProvider.refreshUnreadConversationCounts(); this.messagesProvider.refreshUnreadConversationCounts(this.siteId);
} }
} }
}, this.siteId); }, this.siteId);
@ -145,10 +145,10 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
*/ */
refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise<any> { refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise<any> {
const promises = []; const promises = [];
promises.push(this.messagesProvider.invalidateDiscussionsCache()); promises.push(this.messagesProvider.invalidateDiscussionsCache(this.siteId));
if (refreshUnreadCounts) { if (refreshUnreadCounts) {
promises.push(this.messagesProvider.invalidateUnreadConversationCounts()); promises.push(this.messagesProvider.invalidateUnreadConversationCounts(this.siteId));
} }
return this.utils.allPromises(promises).finally(() => { return this.utils.allPromises(promises).finally(() => {
@ -171,7 +171,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
const promises = []; const promises = [];
promises.push(this.messagesProvider.getDiscussions().then((discussions) => { promises.push(this.messagesProvider.getDiscussions(this.siteId).then((discussions) => {
// Convert to an array for sorting. // Convert to an array for sorting.
const discussionsSorted = []; const discussionsSorted = [];
for (const userId in discussions) { 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) => { return Promise.all(promises).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
@ -216,7 +216,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
this.loaded = false; this.loaded = false;
this.loadingMessage = this.search.loading; 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.showResults = true;
this.search.results = searchResults.messages; this.search.results = searchResults.messages;
}).catch((error) => { }).catch((error) => {

View File

@ -66,11 +66,14 @@
"unabletomessage": "You are unable to message this user", "unabletomessage": "You are unable to message this user",
"unblockuser": "Unblock user", "unblockuser": "Unblock user",
"unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", "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", "userwouldliketocontactyou": "{{$a}} would like to contact you",
"warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", "warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}",
"warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", "warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}",
"wouldliketocontactyou": "Would like to contact you", "wouldliketocontactyou": "Would like to contact you",
"you": "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}}" "yourcontactrequestpending": "Your contact request is pending with {{$a}}"
} }

View File

@ -109,11 +109,20 @@ export class AddonMessagesModule {
messagesProvider.invalidateDiscussionsCache(notification.site).finally(() => { messagesProvider.invalidateDiscussionsCache(notification.site).finally(() => {
// Check if group messaging is enabled, to determine which page should be loaded. // Check if group messaging is enabled, to determine which page should be loaded.
messagesProvider.isGroupMessagingEnabledInSite(notification.site).then((enabled) => { messagesProvider.isGroupMessagingEnabledInSite(notification.site).then((enabled) => {
const pageParams: any = {};
let pageName = 'AddonMessagesIndexPage'; let pageName = 'AddonMessagesIndexPage';
if (enabled) { if (enabled) {
pageName = 'AddonMessagesGroupConversationsPage'; 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);
}); });
}); });
}); });

View File

@ -27,7 +27,7 @@
<ion-avatar core-user-avatar [user]="member" [linkProfile]="false" [checkOnline]="member.showonlinestatus" item-start></ion-avatar> <ion-avatar core-user-avatar [user]="member" [linkProfile]="false" [checkOnline]="member.showonlinestatus" item-start></ion-avatar>
<h2> <h2>
<core-format-text [text]="member.fullname"></core-format-text> <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> </h2>
</a> </a>

View File

@ -352,8 +352,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
} }
// Check if we are at the bottom to scroll it after render. // Check if we are at the bottom to scroll it after render.
this.scrollBottom = this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) === // Use a 5px error margin because in iOS there is 1px difference for some reason.
this.domUtils.getContentHeight(this.content); this.scrollBottom = Math.abs(this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) -
this.domUtils.getContentHeight(this.content)) < 5;
if (this.messagesBeingSent > 0) { if (this.messagesBeingSent > 0) {
// Ignore polling due to a race condition. // Ignore polling due to a race condition.

View File

@ -100,7 +100,7 @@
<h2> <h2>
<core-format-text [text]="conversation.name"></core-format-text> <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> </h2>
<ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount"> <ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount">
<ion-badge *ngIf="conversation.unreadcount > 0">{{ conversation.unreadcount }}</ion-badge> <ion-badge *ngIf="conversation.unreadcount > 0">{{ conversation.unreadcount }}</ion-badge>

View File

@ -70,6 +70,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
protected siteId: string; protected siteId: string;
protected currentUserId: number; protected currentUserId: number;
protected conversationId: number; protected conversationId: number;
protected discussionUserId: number;
protected newMessagesObserver: any; protected newMessagesObserver: any;
protected pushObserver: any; protected pushObserver: any;
protected appResumeSubscription: any; protected appResumeSubscription: any;
@ -89,7 +90,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
this.loadingString = translate.instant('core.loading'); this.loadingString = translate.instant('core.loading');
this.siteId = sitesProvider.getCurrentSiteId(); this.siteId = sitesProvider.getCurrentSiteId();
this.currentUserId = sitesProvider.getCurrentSiteUserId(); this.currentUserId = sitesProvider.getCurrentSiteUserId();
// Conversation to load.
this.conversationId = navParams.get('conversationId') || false; this.conversationId = navParams.get('conversationId') || false;
this.discussionUserId = !this.conversationId && (navParams.get('discussionUserId') || false);
// Update conversations when new message is received. // Update conversations when new message is received.
this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => { this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => {
@ -138,8 +141,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
conversation.unreadcount = 0; conversation.unreadcount = 0;
// Conversations changed, invalidate them and refresh unread counts. // Conversations changed, invalidate them and refresh unread counts.
this.messagesProvider.invalidateConversations(); this.messagesProvider.invalidateConversations(this.siteId);
this.messagesProvider.refreshUnreadConversationCounts(); this.messagesProvider.refreshUnreadConversationCounts(this.siteId);
} }
} }
}, this.siteId); }, this.siteId);
@ -213,13 +216,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* Component loaded. * Component loaded.
*/ */
ngOnInit(): void { ngOnInit(): void {
if (this.conversationId) { if (this.conversationId || this.discussionUserId) {
// There is a discussion to load, open the discussion in a new state. // 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(() => { this.fetchData().then(() => {
if (!this.conversationId && this.splitviewCtrl.isOn()) { if (!this.conversationId && !this.discussionUserId && this.splitviewCtrl.isOn()) {
// Load the first conversation. // Load the first conversation.
let conversation; let conversation;
const expandedOption = this.getExpandedOption(); const expandedOption = this.getExpandedOption();
@ -248,12 +251,12 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
const promises = []; const promises = [];
promises.push(this.fetchConversationCounts()); 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(() => { return Promise.all(promises).then(() => {
if (typeof this.favourites.expanded == 'undefined') { if (typeof this.favourites.expanded == 'undefined') {
// The expanded status hasn't been initialized. Do it now. // The expanded status hasn't been initialized. Do it now.
if (this.conversationId) { if (this.conversationId || this.discussionUserId) {
// A certain conversation should be opened. // 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. // We don't know which option it belongs to, so we need to fetch the data for all of them.
const promises = []; const promises = [];
@ -264,7 +267,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
// All conversations have been loaded, find the one we need to load and expand its option. // 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) { if (conversation) {
const option = this.getConversationOption(conversation); const option = this.getConversationOption(conversation);
@ -320,7 +323,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
promises.push(this.fetchConversationCounts()); promises.push(this.fetchConversationCounts());
if (refreshUnreadCounts) { 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); return Promise.all(promises);
@ -344,10 +348,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
offlineMessages; offlineMessages;
// Get the conversations and, if needed, the offline messages. Always try to get the latest data. // 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. // Shouldn't happen.
}).then(() => { }).then(() => {
return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom); return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom, this.siteId);
}).then((result) => { }).then((result) => {
data = result; data = result;
})); }));
@ -359,7 +363,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
promises.push(this.fetchConversationCounts()); promises.push(this.fetchConversationCounts());
if (refreshUnreadCounts) { 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> { protected fetchConversationCounts(): Promise<void> {
// Always try to get the latest data. // Always try to get the latest data.
return this.messagesProvider.invalidateConversationCounts().catch(() => { return this.messagesProvider.invalidateConversationCounts(this.siteId).catch(() => {
// Shouldn't happen. // Shouldn't happen.
}).then(() => { }).then(() => {
return this.messagesProvider.getConversationCounts(); return this.messagesProvider.getConversationCounts(this.siteId);
}).then((counts) => { }).then((counts) => {
this.favourites.count = counts.favourites; this.favourites.count = counts.favourites;
this.individual.count = counts.individual; this.individual.count = counts.individual;
@ -607,7 +612,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise<any> { refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise<any> {
// Don't invalidate conversations and so, they always try to get latest data. // Don't invalidate conversations and so, they always try to get latest data.
const promises = [ const promises = [
this.messagesProvider.invalidateContactRequestsCountCache() this.messagesProvider.invalidateContactRequestsCountCache(this.siteId)
]; ];
return this.utils.allPromises(promises).finally(() => { return this.utils.allPromises(promises).finally(() => {

View File

@ -34,7 +34,7 @@
<ion-avatar item-start core-user-avatar [user]="result" [checkOnline]="true" [linkProfile]="false"></ion-avatar> <ion-avatar item-start core-user-avatar [user]="result" [checkOnline]="true" [linkProfile]="false"></ion-avatar>
<h2> <h2>
<core-format-text [text]="result.fullname"></core-format-text> <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> </h2>
<ion-note *ngIf="result.lastmessagedate > 0"> <ion-note *ngIf="result.lastmessagedate > 0">
{{result.lastmessagedate | coreDateDayOrTime}} {{result.lastmessagedate | coreDateDayOrTime}}

View File

@ -32,6 +32,7 @@
</ion-list> </ion-list>
</ion-card> </ion-card>
<!-- Notifications. -->
<ng-container *ngIf="preferences"> <ng-container *ngIf="preferences">
<div *ngFor="let component of preferences.components"> <div *ngFor="let component of preferences.components">
<ion-card list *ngFor="let notification of component.notifications"> <ion-card list *ngFor="let notification of component.notifications">
@ -90,5 +91,20 @@
</ion-card> </ion-card>
</div> </div>
</ng-container> </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> </core-loading>
</ion-content> </ion-content>

View File

@ -16,8 +16,12 @@ import { Component, OnDestroy } from '@angular/core';
import { IonicPage } from 'ionic-angular'; import { IonicPage } from 'ionic-angular';
import { AddonMessagesProvider } from '../../providers/messages'; import { AddonMessagesProvider } from '../../providers/messages';
import { CoreUserProvider } from '@core/user/providers/user'; 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 { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreConstants } from '@core/constants';
/** /**
* Page that displays the messages settings page. * Page that displays the messages settings page.
@ -39,16 +43,27 @@ export class AddonMessagesSettingsPage implements OnDestroy {
courseMemberValue = AddonMessagesProvider.MESSAGE_PRIVACY_COURSEMEMBER; courseMemberValue = AddonMessagesProvider.MESSAGE_PRIVACY_COURSEMEMBER;
siteValue = AddonMessagesProvider.MESSAGE_PRIVACY_SITE; siteValue = AddonMessagesProvider.MESSAGE_PRIVACY_SITE;
groupMessagingEnabled: boolean; groupMessagingEnabled: boolean;
sendOnEnter: boolean;
isDesktop: boolean;
isMac: boolean;
protected previousContactableValue: number | boolean; protected previousContactableValue: number | boolean;
constructor(private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, 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(); const currentSite = sitesProvider.getCurrentSite();
this.advancedContactable = currentSite && currentSite.isVersionGreaterEqualThan('3.6'); this.advancedContactable = currentSite && currentSite.isVersionGreaterEqualThan('3.6');
this.allowSiteMessaging = currentSite && currentSite.canUseAdvancedFeature('messagingallusers'); this.allowSiteMessaging = currentSite && currentSite.canUseAdvancedFeature('messagingallusers');
this.groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); 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. * Page destroyed.
*/ */

View File

@ -14,7 +14,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -29,7 +29,10 @@ export class AddonMessagesOfflineProvider {
// Variables for database. // Variables for database.
static MESSAGES_TABLE = 'addon_messages_offline_messages'; // When group messaging isn't available or a new conversation starts. 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. static CONVERSATION_MESSAGES_TABLE = 'addon_messages_offline_conversation_messages'; // Conversation messages.
protected tablesSchema = [ protected siteSchema: CoreSiteSchema = {
name: 'AddonMessagesOfflineProvider',
version: 1,
tables: [
{ {
name: AddonMessagesOfflineProvider.MESSAGES_TABLE, name: AddonMessagesOfflineProvider.MESSAGES_TABLE,
columns: [ columns: [
@ -82,12 +85,13 @@ export class AddonMessagesOfflineProvider {
], ],
primaryKeys: ['conversationid', 'text', 'timecreated'] primaryKeys: ['conversationid', 'text', 'timecreated']
} }
]; ]
};
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider,
private textUtils: CoreTextUtilsProvider) { private textUtils: CoreTextUtilsProvider) {
this.logger = logger.getInstance('AddonMessagesOfflineProvider'); this.logger = logger.getInstance('AddonMessagesOfflineProvider');
this.sitesProvider.createTablesFromSchema(this.tablesSchema); this.sitesProvider.registerSiteSchema(this.siteSchema);
} }
/** /**

View File

@ -3,6 +3,7 @@
<core-context-menu> <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="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="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]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [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> </div>
<!-- User can view all submissions (teacher). --> <!-- 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"> <ion-item text-wrap *ngIf="timeRemaining">
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
<p>{{ timeRemaining }}</p> <p>{{ timeRemaining }}</p>
@ -79,7 +88,7 @@
<ion-icon name="information-circle"></ion-icon> <ion-icon name="information-circle"></ion-icon>
{{ 'addon.mod_assign.ungroupedusers' | translate }} {{ 'addon.mod_assign.ungroupedusers' | translate }}
</div> </div>
</ion-card> </ion-list>
<!-- If it's a student, display his submission. --> <!-- 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> <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" [moduleId]="module.id"></addon-mod-assign-submission>

View File

@ -14,7 +14,7 @@
import { Component, Optional, Injector, ViewChild } from '@angular/core'; import { Component, Optional, Injector, ViewChild } from '@angular/core';
import { Content, NavController } from 'ionic-angular'; import { Content, NavController } from 'ionic-angular';
import { CoreGroupsProvider } from '@providers/groups'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { AddonModAssignProvider } from '../../providers/assign'; import { AddonModAssignProvider } from '../../providers/assign';
@ -45,6 +45,12 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
summary: any; // The summary. summary: any; // The summary.
needsGradingAvalaible: boolean; // Whether we can see the submissions that need grading. needsGradingAvalaible: boolean; // Whether we can see the submissions that need grading.
groupInfo: CoreGroupInfo = {
groups: [],
separateGroups: false,
visibleGroups: false
};
// Status. // Status.
submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED; submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED;
submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT; submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT;
@ -193,15 +199,13 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
} }
// Check if groupmode is enabled to avoid showing wrong numbers. // Check if groupmode is enabled to avoid showing wrong numbers.
return this.groupsProvider.activityHasGroups(this.assign.cmid).then((hasGroups) => { return this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false).then((groupInfo) => {
this.showNumbers = !hasGroups; this.groupInfo = groupInfo;
this.showNumbers = groupInfo.groups.length == 0 ||
this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.5');
return this.assignProvider.getSubmissionStatus(this.assign.id).then((response) => { return this.setGroup(this.group || (groupInfo.groups && groupInfo.groups[0] && groupInfo.groups[0].id) ||
this.summary = response.gradingsummary; 0);
this.needsGradingAvalaible = response.gradingsummary.submissionsneedgradingcount > 0 &&
this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.2');
});
}); });
} }
@ -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. * Go to view a list of submissions.
* *
@ -232,6 +253,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
if (typeof status == 'undefined') { if (typeof status == 'undefined') {
this.navCtrl.push('AddonModAssignSubmissionListPage', { this.navCtrl.push('AddonModAssignSubmissionListPage', {
courseId: this.courseId, courseId: this.courseId,
groupId: this.group || 0,
moduleId: this.module.id, moduleId: this.module.id,
moduleName: this.moduleName moduleName: this.moduleName
}); });
@ -239,6 +261,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
this.navCtrl.push('AddonModAssignSubmissionListPage', { this.navCtrl.push('AddonModAssignSubmissionListPage', {
status: status, status: status,
courseId: this.courseId, courseId: this.courseId,
groupId: this.group || 0,
moduleId: this.module.id, moduleId: this.module.id,
moduleName: this.moduleName moduleName: this.moduleName
}); });
@ -273,7 +296,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
promises.push(this.assignProvider.invalidateAllSubmissionData(this.assign.id)); promises.push(this.assignProvider.invalidateAllSubmissionData(this.assign.id));
if (this.canViewAllSubmissions) { if (this.canViewAllSubmissions) {
promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id)); promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, undefined, this.group));
} }
} }

View File

@ -215,6 +215,12 @@
<p *ngIf="feedback.gradeddate">{{ feedback.gradeddate * 1000 | coreFormatDate }}</p> <p *ngIf="feedback.gradeddate">{{ feedback.gradeddate * 1000 | coreFormatDate }}</p>
</a> </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. --> <!-- Warning message if cannot save grades. -->
<div *ngIf="isGrading && !canSaveGrades" class="core-warning-card" icon-start> <div *ngIf="isGrading && !canSaveGrades" class="core-warning-card" icon-start>
<ion-icon name="warning"></ion-icon> <ion-icon name="warning"></ion-icon>

View File

@ -338,7 +338,8 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); promises.push(this.assignProvider.invalidateAssignmentData(this.courseId));
if (this.assign) { 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.invalidateAssignmentUserMappingsData(this.assign.id));
promises.push(this.assignProvider.invalidateListParticipantsData(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); return Promise.all(promises);
}).then(() => { }).then(() => {
// Get submission status. // 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) => { }).then((response) => {
const promises = []; const promises = [];
@ -485,12 +486,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
this.feedback = feedback; this.feedback = feedback;
// If we have data about the grader, get its profile. // 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.userProvider.getProfile(feedback.grade.grader, this.courseId).then((profile) => {
this.grader = profile; this.grader = profile;
}).catch(() => { }).catch(() => {
// Ignore errors. // Ignore errors.
}); });
} else {
delete this.grader;
} }
// Check if the grade uses advanced grading. // Check if the grade uses advanced grading.

View File

@ -121,9 +121,11 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
}).then(() => { }).then(() => {
// Get submission status. Ignore cache to get the latest data. // Get submission status. Ignore cache to get the latest data.
return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, 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. // 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); const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt);
// Check if the user can edit it in offline. // Check if the user can edit it in offline.
@ -303,6 +305,9 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
} }
return promise.then(() => { return promise.then(() => {
// Clear temporary data from plugins.
return this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData);
}).then(() => {
// Submission saved, trigger event. // Submission saved, trigger event.
const params = { const params = {
assignmentId: this.assign.id, assignmentId: this.assign.id,

View File

@ -15,10 +15,17 @@
</core-empty-box> </core-empty-box>
<ion-list> <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. --> <!-- List of submissions. -->
<ng-container *ngFor="let submission 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"> <a ion-item text-wrap (click)="loadSubmission(submission)" [class.core-split-item-selected]="submission.submitid == selectedSubmissionId">
<ion-avatar core-user-avatar [user]="submission" item-start></ion-avatar> <ion-avatar core-user-avatar [user]="submission" [linkProfile]="false" item-start></ion-avatar>
<h2 *ngIf="submission.userfullname">{{submission.userfullname}}</h2> <h2 *ngIf="submission.userfullname">{{submission.userfullname}}</h2>
<h2 *ngIf="!submission.userfullname">{{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}</h2> <h2 *ngIf="!submission.userfullname">{{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}</h2>
<p *ngIf="assign.teamsubmission"> <p *ngIf="assign.teamsubmission">

View File

@ -18,6 +18,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
import { AddonModAssignProvider } from '../../providers/assign'; import { AddonModAssignProvider } from '../../providers/assign';
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
import { AddonModAssignHelperProvider } from '../../providers/helper'; import { AddonModAssignHelperProvider } from '../../providers/helper';
@ -40,19 +41,28 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
loaded: boolean; // Whether data has been loaded. loaded: boolean; // Whether data has been loaded.
haveAllParticipants: boolean; // Whether all participants have been loaded. haveAllParticipants: boolean; // Whether all participants have been loaded.
selectedSubmissionId: number; // Selected submission ID. 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 moduleId: number; // Module ID the submission belongs to.
protected courseId: number; // Course ID the assignment belongs to. protected courseId: number; // Course ID the assignment belongs to.
protected selectedStatus: string; // The status to see. protected selectedStatus: string; // The status to see.
protected gradedObserver; // Observer to refresh data when a grade changes. protected gradedObserver; // Observer to refresh data when a grade changes.
protected submissionsData: any;
constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService,
protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
protected assignHelper: AddonModAssignHelperProvider) { protected assignHelper: AddonModAssignHelperProvider, protected groupsProvider: CoreGroupsProvider) {
this.moduleId = navParams.get('moduleId'); this.moduleId = navParams.get('moduleId');
this.courseId = navParams.get('courseId'); this.courseId = navParams.get('courseId');
this.groupId = navParams.get('groupId');
this.selectedStatus = navParams.get('status'); this.selectedStatus = navParams.get('status');
if (this.selectedStatus) { if (this.selectedStatus) {
@ -98,15 +108,11 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
protected fetchAssignment(): Promise<any> { protected fetchAssignment(): Promise<any> {
let participants,
submissionsData,
grades;
// Get assignment data. // Get assignment data.
return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => {
this.title = assign.name || this.title; this.title = assign.name || this.title;
this.assign = assign; this.assign = assign;
this.haveAllParticipants = true;
// Get assignment submissions. // Get assignment submissions.
return this.assignProvider.getSubmissions(assign.id); return this.assignProvider.getSubmissions(assign.id);
@ -116,15 +122,39 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
return Promise.reject(null); return Promise.reject(null);
} }
submissionsData = data; this.submissionsData = data;
// 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. // Get the participants.
return this.assignHelper.getParticipants(this.assign).then((parts) => { return this.assignHelper.getParticipants(this.assign, this.groupId).then((parts) => {
this.haveAllParticipants = true; this.haveAllParticipants = true;
participants = parts; participants = parts;
}).catch(() => { }).catch(() => {
this.haveAllParticipants = false; this.haveAllParticipants = false;
});
}).then(() => { }).then(() => {
if (!this.assign.markingworkflow) { if (!this.assign.markingworkflow) {
// Get assignment grades only if workflow is not enabled to check grading date. // Get assignment grades only if workflow is not enabled to check grading date.
@ -134,16 +164,16 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
} }
}).then(() => { }).then(() => {
// We want to show the user data on each submission. // 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); this.assign.blindmarking && !this.assign.revealidentities, participants);
}).then((submissions) => { }).then((submissions) => {
// Filter the submissions to get only the ones with the right status and add some extra data. // Filter the submissions to get only the ones with the right status and add some extra data.
const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING, const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING,
searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus, searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus,
promises = []; promises = [],
showSubmissions = [];
this.submissions = [];
submissions.forEach((submission) => { submissions.forEach((submission) => {
if (!searchStatus || searchStatus == submission.status) { if (!searchStatus || searchStatus == submission.status) {
promises.push(this.assignOfflineProvider.getSubmissionGrade(this.assign.id, submission.userid).catch(() => { promises.push(this.assignOfflineProvider.getSubmissionGrade(this.assign.id, submission.userid).catch(() => {
@ -203,15 +233,15 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
submission.gradingStatusTranslationId = false; submission.gradingStatusTranslationId = false;
} }
this.submissions.push(submission); showSubmissions.push(submission);
}); });
})); }));
} }
}); });
return Promise.all(promises); return Promise.all(promises).then(() => {
}).catch((error) => { this.submissions = showSubmissions;
this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); });
}); });
} }
@ -221,12 +251,12 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
* @param {any} submission The submission to load. * @param {any} submission The submission to load.
*/ */
loadSubmission(submission: any): void { loadSubmission(submission: any): void {
if (this.selectedSubmissionId === submission.id && this.splitviewCtrl.isOn()) { if (this.selectedSubmissionId === submission.submitid && this.splitviewCtrl.isOn()) {
// Already selected. // Already selected.
return; return;
} }
this.selectedSubmissionId = submission.id; this.selectedSubmissionId = submission.submitid;
this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', { this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', {
courseId: this.courseId, courseId: this.courseId,

View File

@ -132,7 +132,8 @@ export class AddonModAssignSubmissionReviewPage implements OnInit {
if (this.assign) { if (this.assign) {
promises.push(this.assignProvider.invalidateSubmissionData(this.assign.id)); promises.push(this.assignProvider.invalidateSubmissionData(this.assign.id));
promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(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(() => { return Promise.all(promises).finally(() => {

View File

@ -15,7 +15,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreFileProvider } from '@providers/file'; import { CoreFileProvider } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
@ -30,7 +30,10 @@ export class AddonModAssignOfflineProvider {
// Variables for database. // Variables for database.
static SUBMISSIONS_TABLE = 'addon_mod_assign_submissions'; static SUBMISSIONS_TABLE = 'addon_mod_assign_submissions';
static SUBMISSIONS_GRADES_TABLE = 'addon_mod_assign_submissions_grading'; static SUBMISSIONS_GRADES_TABLE = 'addon_mod_assign_submissions_grading';
protected tablesSchema = [ protected siteSchema: CoreSiteSchema = {
name: 'AddonModAssignOfflineProvider',
version: 1,
tables: [
{ {
name: AddonModAssignOfflineProvider.SUBMISSIONS_TABLE, name: AddonModAssignOfflineProvider.SUBMISSIONS_TABLE,
columns: [ columns: [
@ -123,12 +126,13 @@ export class AddonModAssignOfflineProvider {
], ],
primaryKeys: ['assignid', 'userid'] primaryKeys: ['assignid', 'userid']
} }
]; ]
};
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
private fileProvider: CoreFileProvider, private timeUtils: CoreTimeUtilsProvider) { private fileProvider: CoreFileProvider, private timeUtils: CoreTimeUtilsProvider) {
this.logger = logger.getInstance('AddonModAssignOfflineProvider'); this.logger = logger.getInstance('AddonModAssignOfflineProvider');
this.sitesProvider.createTablesFromSchema(this.tablesSchema); this.sitesProvider.registerSiteSchema(this.siteSchema);
} }
/** /**

View File

@ -23,6 +23,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreSyncBaseProvider } from '@classes/base-sync';
import { AddonModAssignProvider } from './assign'; import { AddonModAssignProvider } from './assign';
@ -61,7 +62,8 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
private courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, private courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider,
private assignProvider: AddonModAssignProvider, private assignOfflineProvider: AddonModAssignOfflineProvider, private assignProvider: AddonModAssignProvider, private assignOfflineProvider: AddonModAssignOfflineProvider,
private utils: CoreUtilsProvider, private submissionDelegate: AddonModAssignSubmissionDelegate, 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, super('AddonModAssignSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate,
timeUtils); timeUtils);
@ -202,6 +204,9 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
return []; return [];
})); }));
// Sync offline logs.
promises.push(this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId));
syncPromise = Promise.all(promises).then((results) => { syncPromise = Promise.all(promises).then((results) => {
const submissions = results[0], const submissions = results[0],
grades = results[1]; grades = results[1];
@ -216,7 +221,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; 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; assign = assignData;
const promises = []; const promises = [];
@ -270,7 +275,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
let discardError, let discardError,
submission; 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 = []; const promises = [];
submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt); submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt);
@ -305,7 +310,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
} }
}).then(() => { }).then(() => {
// Submission data sent, update cached data. No need to block the user for this. // Submission data sent, update cached data. No need to block the user for this.
this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId); this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId);
}); });
}).catch((error) => { }).catch((error) => {
if (error && this.utils.isWebServiceError(error)) { if (error && this.utils.isWebServiceError(error)) {
@ -359,7 +364,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
const userId = offlineData.userid; const userId = offlineData.userid;
let discardError; 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); const timemodified = status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified);
if (timemodified > offlineData.timemodified) { if (timemodified > offlineData.timemodified) {
@ -400,7 +405,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
offlineData.plugindata, siteId).then(() => { offlineData.plugindata, siteId).then(() => {
// Grades sent, update cached data. No need to block the user for this. // 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) => { }).catch((error) => {
if (error && this.utils.isWebServiceError(error)) { if (error && this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means it cannot be submitted. Discard the offline data. // The WebService has thrown an error, this means it cannot be submitted. Discard the offline data.

View File

@ -21,8 +21,8 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { CoreCommentsProvider } from '@core/comments/providers/comments';
import { CoreUserProvider } from '@core/user/providers/user';
import { CoreGradesProvider } from '@core/grades/providers/grades'; import { CoreGradesProvider } from '@core/grades/providers/grades';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignSubmissionDelegate } from './submission-delegate';
import { AddonModAssignOfflineProvider } from './assign-offline'; import { AddonModAssignOfflineProvider } from './assign-offline';
import { CoreSiteWSPreSets } from '@classes/site'; import { CoreSiteWSPreSets } from '@classes/site';
@ -66,9 +66,10 @@ export class AddonModAssignProvider {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
private timeUtils: CoreTimeUtilsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, 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 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'); this.logger = logger.getInstance('AddonModAssignProvider');
} }
@ -118,11 +119,12 @@ export class AddonModAssignProvider {
* *
* @param {number} courseId Course ID the assignment belongs to. * @param {number} courseId Course ID the assignment belongs to.
* @param {number} cmId Assignment module ID. * @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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the assignment. * @return {Promise<any>} Promise resolved with the assignment.
*/ */
getAssignment(courseId: number, cmId: number, siteId?: string): Promise<any> { getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
return this.getAssignmentByField(courseId, 'cmid', cmId, siteId); return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId);
} }
/** /**
@ -131,19 +133,27 @@ export class AddonModAssignProvider {
* @param {number} courseId Course ID. * @param {number} courseId Course ID.
* @param {string} key Name of the property to check. * @param {string} key Name of the property to check.
* @param {any} value Value to search. * @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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the assignment is retrieved. * @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) => { return this.sitesProvider.getSite(siteId).then((site) => {
const params = { const params = {
courseids: [courseId], courseids: [courseId],
includenotenrolledcourses: 1 includenotenrolledcourses: 1
}, },
preSets = { preSets: CoreSiteWSPreSets = {
cacheKey: this.getAssignmentCacheKey(courseId) cacheKey: this.getAssignmentCacheKey(courseId)
}; };
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_assign_get_assignments', params, preSets).catch(() => { return site.read('mod_assign_get_assignments', params, preSets).catch(() => {
// In 3.6 we added a new parameter includenotenrolledcourses that could cause offline data not to be found. // In 3.6 we added a new parameter includenotenrolledcourses that could cause offline data not to be found.
// Retry again without the param to check if the request is already cached. // 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} courseId Course ID the assignment belongs to.
* @param {number} cmId Assignment instance ID. * @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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the assignment. * @return {Promise<any>} Promise resolved with the assignment.
*/ */
getAssignmentById(courseId: number, id: number, siteId?: string): Promise<any> { getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
return this.getAssignmentByField(courseId, 'id', id, siteId); return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId);
} }
/** /**
@ -194,18 +205,24 @@ export class AddonModAssignProvider {
* *
* @param {number} assignId Assignment Id. * @param {number} assignId Assignment Id.
* @param {number} userId User Id to be blinded. * @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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<number>} Promise resolved with the user blind id. * @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) => { return this.sitesProvider.getSite(siteId).then((site) => {
const params = { const params = {
assignmentids: [assignId] assignmentids: [assignId]
}, },
preSets = { preSets: CoreSiteWSPreSets = {
cacheKey: this.getAssignmentUserMappingsCacheKey(assignId) cacheKey: this.getAssignmentUserMappingsCacheKey(assignId)
}; };
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_assign_get_user_mappings', params, preSets).then((response) => { return site.read('mod_assign_get_user_mappings', params, preSets).then((response) => {
// Search the user. // Search the user.
if (response.assignments && response.assignments.length) { if (response.assignments && response.assignments.length) {
@ -248,18 +265,24 @@ export class AddonModAssignProvider {
* Returns grade information from assign_grades for the requested assignment id * Returns grade information from assign_grades for the requested assignment id
* *
* @param {number} assignId 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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved with requested info when done. * @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) => { return this.sitesProvider.getSite(siteId).then((site) => {
const params = { const params = {
assignmentids: [assignId] assignmentids: [assignId]
}, },
preSets = { preSets: CoreSiteWSPreSets = {
cacheKey: this.getAssignmentGradesCacheKey(assignId) cacheKey: this.getAssignmentGradesCacheKey(assignId)
}; };
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_assign_get_grades', params, preSets).then((response) => { return site.read('mod_assign_get_grades', params, preSets).then((response) => {
// Search the assignment. // Search the assignment.
if (response.assignments && response.assignments.length) { if (response.assignments && response.assignments.length) {
@ -356,9 +379,13 @@ export class AddonModAssignProvider {
* *
* @param {any} assign Assign. * @param {any} assign Assign.
* @param {any} attempt Attempt. * @param {any} attempt Attempt.
* @return {any} Submission object. * @return {any} Submission object or null.
*/ */
getSubmissionObjectFromAttempt(assign: any, attempt: any): any { getSubmissionObjectFromAttempt(assign: any, attempt: any): any {
if (!attempt) {
return null;
}
return assign.teamsubmission ? attempt.teamsubmission : attempt.submission; return assign.teamsubmission ? attempt.teamsubmission : attempt.submission;
} }
@ -419,18 +446,26 @@ export class AddonModAssignProvider {
* Get an assignment submissions. * Get an assignment submissions.
* *
* @param {number} assignId 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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<{canviewsubmissions: boolean, submissions?: any[]}>} Promise resolved when done. * @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) => { return this.sitesProvider.getSite(siteId).then((site) => {
const params = { const params = {
assignmentids: [assignId] assignmentids: [assignId]
}, },
preSets = { preSets: CoreSiteWSPreSets = {
cacheKey: this.getSubmissionsCacheKey(assignId) cacheKey: this.getSubmissionsCacheKey(assignId)
}; };
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_assign_get_submissions', params, preSets).then((response): any => { return site.read('mod_assign_get_submissions', params, preSets).then((response): any => {
// Check if we can view submissions, with enough permissions. // Check if we can view submissions, with enough permissions.
if (response.warnings.length > 0 && response.warnings[0].warningcode == 1) { 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. * Get information about an assignment submission status for a given user.
* *
* @param {number} assignId Assignment instance id. * @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 {boolean} [isBlind] If blind marking is enabled or not.
* @param {number} [filter=true] True to filter WS response and rewrite URLs, false otherwise. * @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 {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). * @param {string} [siteId] Site id (empty for current site).
* @return {Promise<any>} Promise always resolved with the user submission status. * @return {Promise<any>} Promise always resolved with the user submission status.
*/ */
getSubmissionStatus(assignId: number, userId?: number, isBlind?: boolean, filter: boolean = true, ignoreCache?: boolean, getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true,
siteId?: string): Promise<any> { ignoreCache?: boolean, siteId?: string): Promise<any> {
userId = userId || 0; userId = userId || 0;
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
groupId = site.isVersionGreaterEqualThan('3.5') ? groupId || 0 : 0;
const params = { const params = {
assignid: assignId, assignid: assignId,
userid: userId userid: userId
}, },
preSets: CoreSiteWSPreSets = { 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. getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account.
filter: filter, filter: filter,
rewriteurls: filter rewriteurls: filter
}; };
if (groupId) {
params['groupid'] = groupId;
}
if (ignoreCache) { if (ignoreCache) {
preSets.getFromCache = false; preSets.getFromCache = false;
preSets.emergencyCache = 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. * Get cache key for get submission status data WS calls.
* *
* @param {number} assignId Assignment instance id. * @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 {number} [isBlind] If blind marking is enabled or not. * @param {number} [isBlind] If blind marking is enabled or not.
* @return {string} Cache key. * @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) { if (!userId) {
isBlind = false; isBlind = false;
userId = this.sitesProvider.getCurrentSiteUserId(); 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 {number} assignId ID of the assignment the submissions belong to.
* @param {boolean} [blind] Whether the user data need to be blinded. * @param {boolean} [blind] Whether the user data need to be blinded.
* @param {any[]} [participants] List of participants in the assignment. * @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). * @param {string} [siteId] Site id (empty for current site).
* @return {Promise<any[]>} Promise always resolved. Resolve param is the formatted submissions. * @return {Promise<any[]>} Promise always resolved. Resolve param is the formatted submissions.
*/ */
getSubmissionsUserData(submissions: any[], courseId: number, assignId: number, blind?: boolean, participants?: any[], getSubmissionsUserData(submissions: any[], courseId: number, assignId: number, blind?: boolean, participants?: any[],
siteId?: string): Promise<any[]> { ignoreCache?: boolean, siteId?: string): Promise<any[]> {
const promises = [], const promises = [],
subs = [], subs = [],
hasParticipants = participants && participants.length > 0; hasParticipants = participants && participants.length > 0;
if (!hasParticipants) {
return Promise.resolve([]);
}
submissions.forEach((submission) => { submissions.forEach((submission) => {
submission.submitid = submission.userid > 0 ? submission.userid : submission.blindid; submission.submitid = submission.userid > 0 ? submission.userid : submission.blindid;
if (submission.submitid <= 0) { if (submission.submitid <= 0) {
@ -568,12 +646,11 @@ export class AddonModAssignProvider {
} }
const participant = this.getParticipantFromUserId(participants, submission.submitid); const participant = this.getParticipantFromUserId(participants, submission.submitid);
if (hasParticipants && !participant) { if (!participant) {
// Avoid permission denied error. Participant not found on list. // Avoid permission denied error. Participant not found on list.
return; return;
} }
if (participant) {
if (!blind) { if (!blind) {
submission.userfullname = participant.fullname; submission.userfullname = participant.fullname;
submission.userprofileimageurl = participant.profileimageurl; submission.userprofileimageurl = participant.profileimageurl;
@ -584,26 +661,15 @@ export class AddonModAssignProvider {
submission.groupid = participant.groupid; submission.groupid = participant.groupid;
submission.groupname = participant.groupname; submission.groupname = participant.groupname;
} }
}
let promise; let promise;
if (submission.userid > 0) { if (submission.userid > 0 && blind) {
if (blind) {
// Blind but not blinded! (Moodle < 3.1.1, 3.2). // Blind but not blinded! (Moodle < 3.1.1, 3.2).
delete submission.userid; delete submission.userid;
promise = this.getAssignmentUserMappings(assignId, submission.submitid, siteId).then((blindId) => { promise = this.getAssignmentUserMappings(assignId, submission.submitid, ignoreCache, siteId).then((blindId) => {
submission.blindid = 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 = promise || Promise.resolve(); promise = promise || Promise.resolve();
@ -675,10 +741,11 @@ export class AddonModAssignProvider {
* *
* @param {number} assignId Assignment id. * @param {number} assignId Assignment id.
* @param {number} [groupId] Group id. If not defined, 0. * @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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with the list of participants and summary of submissions. * @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; groupId = groupId || 0;
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
@ -692,10 +759,15 @@ export class AddonModAssignProvider {
groupid: groupId, groupid: groupId,
filter: '' filter: ''
}, },
preSets = { preSets: CoreSiteWSPreSets = {
cacheKey: this.listParticipantsCacheKey(assignId, groupId) cacheKey: this.listParticipantsCacheKey(assignId, groupId)
}; };
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_assign_list_participants', params, preSets); return site.read('mod_assign_list_participants', params, preSets);
}); });
} }
@ -785,7 +857,7 @@ export class AddonModAssignProvider {
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> { invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.getAssignment(courseId, moduleId, siteId).then((assign) => { return this.getAssignment(courseId, moduleId, false, siteId).then((assign) => {
const promises = []; const promises = [];
// Do not invalidate assignment data before getting assignment info, we need it! // Do not invalidate assignment data before getting assignment info, we need it!
@ -830,13 +902,15 @@ export class AddonModAssignProvider {
* *
* @param {number} assignId Assignment instance id. * @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] Whether blind marking is enabled or not. * @param {boolean} [isBlind] Whether blind marking is enabled or not.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated. * @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 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. * @return {Promise<any>} Promise resolved when the WS call is successful.
*/ */
logGradingView(assignId: number, siteId?: string): Promise<any> { logGradingView(assignId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = { const params = {
assignid: assignId 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. * @return {Promise<any>} Promise resolved when the WS call is successful.
*/ */
logView(assignId: number, siteId?: string): Promise<any> { logView(assignId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = { const params = {
assignid: assignId 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. // 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) { if (!response.feedback || !response.feedback.gradeddate) {
// Not graded. // Not graded.
return true; return true;

View File

@ -149,26 +149,29 @@ export class AddonModAssignHelperProvider {
/** /**
* List the participants for a single assignment, with some summary info about their submissions. * 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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]} Promise resolved with the list of participants and summary of submissions. * @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(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Get the participants without specifying a group. return this.assignProvider.listParticipants(assign.id, groupId, ignoreCache, siteId).then((participants) => {
return this.assignProvider.listParticipants(assign.id, undefined, siteId).then((participants) => { if (groupId || participants && participants.length > 0) {
if (participants && participants.length > 0) {
return participants; 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) => { return this.groupsProvider.getActivityAllowedGroupsIfEnabled(assign.cmid, undefined, siteId).then((userGroups) => {
const promises = [], const promises = [],
participants = {}; participants = {};
userGroups.forEach((userGroup) => { 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. // Do not get repeated users.
parts.forEach((participant) => { parts.forEach((participant) => {
participants[participant.id] = participant; participants[participant.id] = participant;

View File

@ -68,8 +68,12 @@ export class AddonModAssignModuleHandler implements CoreCourseModuleHandler {
title: module.name, title: module.name,
class: 'addon-mod_assign-handler', class: 'addon-mod_assign-handler',
showDownloadButton: true, showDownloadButton: true,
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void {
navCtrl.push('AddonModAssignIndexPage', {module: module, courseId: courseId}, options); const pageParams = {module: module, courseId: courseId};
if (params) {
Object.assign(pageParams, params);
}
navCtrl.push('AddonModAssignIndexPage', pageParams, options);
} }
}; };
} }

View File

@ -92,19 +92,19 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
siteId = siteId || this.sitesProvider.getCurrentSiteId(); 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. // Get intro files and attachments.
let files = assign.introattachments || []; let files = assign.introattachments || [];
files = files.concat(this.getIntroFilesFromInstance(module, assign)); files = files.concat(this.getIntroFilesFromInstance(module, assign));
// Now get the files in the submissions. // Now get the files in the submissions.
return this.assignProvider.getSubmissions(assign.id, siteId).then((data) => { return this.assignProvider.getSubmissions(assign.id, false, siteId).then((data) => {
const blindMarking = assign.blindmarking && !assign.revealidentities; const blindMarking = assign.blindmarking && !assign.revealidentities;
if (data.canviewsubmissions) { if (data.canviewsubmissions) {
// Teacher, get all submissions. // Teacher, get all submissions.
return this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id, blindMarking, return this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id, blindMarking,
undefined, siteId).then((submissions) => { undefined, false, siteId).then((submissions) => {
const promises = []; const promises = [];
@ -156,7 +156,8 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string) protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string)
: Promise<any[]> { : Promise<any[]> {
return this.assignProvider.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 = []; const promises = [];
if (response.lastattempt) { if (response.lastattempt) {
@ -200,6 +201,17 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
return this.assignProvider.invalidateContent(moduleId, courseId); 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. * 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(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Get assignment to retrieve all its submissions. // 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 = [], const subPromises = [],
blindMarking = assign.blindmarking && !assign.revealidentities; blindMarking = assign.blindmarking && !assign.revealidentities;
if (blindMarking) { 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. // Ignore errors.
})); }));
} }
@ -252,10 +264,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
subPromises.push(this.courseHelper.getModuleCourseIdByInstance(assign.id, 'assign', siteId)); subPromises.push(this.courseHelper.getModuleCourseIdByInstance(assign.id, 'assign', siteId));
// Get all files and fetch them. // Download intro files and attachments. Do not call getFiles because it'd call some WS twice.
subPromises.push(this.getFiles(module, courseId, single, siteId).then((files) => { let files = assign.introattachments || [];
return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); files = files.concat(this.getIntroFilesFromInstance(module, assign));
}));
subPromises.push(this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id));
return Promise.all(subPromises); return Promise.all(subPromises);
})); }));
@ -274,22 +287,28 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
* @return {Promise<any>} Promise resolved when prefetched, rejected otherwise. * @return {Promise<any>} Promise resolved when prefetched, rejected otherwise.
*/ */
protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise<any> { protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise<any> {
// Get submissions. // Get submissions.
return this.assignProvider.getSubmissions(assign.id, siteId).then((data) => { return this.assignProvider.getSubmissions(assign.id, true, siteId).then((data) => {
const promises = [], const promises = [],
blindMarking = assign.blindmarking && !assign.revealidentities; blindMarking = assign.blindmarking && !assign.revealidentities;
if (data.canviewsubmissions) { if (data.canviewsubmissions) {
// Teacher. Do not send participants to getSubmissionsUserData to retrieve user profiles. // Teacher. Do not send participants to getSubmissionsUserData to retrieve user profiles.
promises.push(this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id, blindMarking, promises.push(this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((groupInfo) => {
undefined, siteId).then((submissions) => { const groupProms = [];
if (!groupInfo.groups || groupInfo.groups.length == 0) {
groupInfo.groups = [{id: 0}];
}
groupInfo.groups.forEach((group) => {
groupProms.push(this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id,
blindMarking, undefined, true, siteId).then((submissions) => {
const subPromises = []; const subPromises = [];
submissions.forEach((submission) => { submissions.forEach((submission) => {
subPromises.push(this.assignProvider.getSubmissionStatus(assign.id, submission.submitid, subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submission.submitid,
!!submission.blindid, true, false, siteId).then((subm) => { group.id, !!submission.blindid, true, true, siteId).then((subm) => {
return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId); return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId);
}).catch((error) => { }).catch((error) => {
if (error && error.errorcode == 'nopermission') { if (error && error.errorcode == 'nopermission') {
@ -303,13 +322,13 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
if (!assign.markingworkflow) { if (!assign.markingworkflow) {
// Get assignment grades only if workflow is not enabled to check grading date. // Get assignment grades only if workflow is not enabled to check grading date.
subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, siteId)); subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, true, siteId));
} }
// Prefetch the submission of the current user even if it does not exist, this will be create it. // Prefetch the submission of the current user even if it does not exist, this will be create it.
if (!data.submissions || !data.submissions.find((subm) => subm.submitid == userId)) { if (!data.submissions || !data.submissions.find((subm) => subm.submitid == userId)) {
subPromises.push(this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, false, siteId) subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, userId, group.id,
.then((subm) => { false, true, true, siteId).then((subm) => {
return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId);
})); }));
} }
@ -318,7 +337,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
})); }));
// Get list participants. // Get list participants.
promises.push(this.assignHelper.getParticipants(assign, siteId).then((participants) => { groupProms.push(this.assignHelper.getParticipants(assign, group.id, true, siteId).then((participants) => {
participants.forEach((participant) => { participants.forEach((participant) => {
if (participant.profileimageurl) { if (participant.profileimageurl) {
this.filepoolProvider.addToQueueByUrl(siteId, participant.profileimageurl); this.filepoolProvider.addToQueueByUrl(siteId, participant.profileimageurl);
@ -327,10 +346,15 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
}).catch(() => { }).catch(() => {
// Fail silently (Moodle < 3.2). // Fail silently (Moodle < 3.2).
})); }));
});
return Promise.all(groupProms);
}));
} else { } else {
// Student. // Student.
promises.push( 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); return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId);
}).catch((error) => { }).catch((error) => {
// Ignore if the user can't view their own submission. // Ignore if the user can't view their own submission.
@ -341,8 +365,8 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
); );
} }
promises.push(this.groupsProvider.activityHasGroups(assign.cmid)); promises.push(this.groupsProvider.activityHasGroups(assign.cmid, siteId, true));
promises.push(this.groupsProvider.getActivityAllowedGroups(assign.cmid, undefined, siteId)); promises.push(this.groupsProvider.getActivityAllowedGroups(assign.cmid, undefined, siteId, true));
return Promise.all(promises); return Promise.all(promises);
}); });
@ -378,7 +402,16 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
// Prefetch submission plugins data. // Prefetch submission plugins data.
if (userSubmission.plugins) { if (userSubmission.plugins) {
userSubmission.plugins.forEach((plugin) => { userSubmission.plugins.forEach((plugin) => {
// Prefetch the plugin WS data.
promises.push(this.submissionDelegate.prefetch(assign, userSubmission, plugin, siteId)); 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. // Prefetch feedback.
if (submission.feedback) { if (submission.feedback) {
// Get profile and image of the grader. // 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); userIds.push(submission.feedback.grade.grader);
} }
if (userId) { 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. // Prefetch feedback plugins data.
if (submission.feedback.plugins) { if (submission.feedback.plugins) {
submission.feedback.plugins.forEach((plugin) => { submission.feedback.plugins.forEach((plugin) => {
// Prefetch the plugin WS data.
promises.push(this.feedbackDelegate.prefetch(assign, submission, plugin, siteId)); 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.
}));
}); });
} }
} }

View File

@ -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."
} }

View File

@ -14,6 +14,7 @@
// limitations under the License. // limitations under the License.
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreWSProvider } from '@providers/ws'; import { CoreWSProvider } from '@providers/ws';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -31,7 +32,7 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign
name = 'AddonModAssignSubmissionOnlineTextHandler'; name = 'AddonModAssignSubmissionOnlineTextHandler';
type = 'onlinetext'; 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 textUtils: CoreTextUtilsProvider, private assignProvider: AddonModAssignProvider,
private assignOfflineProvider: AddonModAssignOfflineProvider, private assignHelper: AddonModAssignHelperProvider) { } private assignOfflineProvider: AddonModAssignOfflineProvider, private assignHelper: AddonModAssignHelperProvider) { }
@ -238,6 +239,19 @@ export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssign
let text = this.getTextToSubmit(plugin, inputData); 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. // Add some HTML to the text if needed.
text = this.textUtils.formatHtmlLines(text); text = this.textUtils.formatHtmlLines(text);

View File

@ -20,12 +20,10 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModBookIndexComponent } from './index/index'; import { AddonModBookIndexComponent } from './index/index';
import { AddonModBookTocPopoverComponent } from './toc-popover/toc-popover';
@NgModule({ @NgModule({
declarations: [ declarations: [
AddonModBookIndexComponent, AddonModBookIndexComponent
AddonModBookTocPopoverComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -38,12 +36,10 @@ import { AddonModBookTocPopoverComponent } from './toc-popover/toc-popover';
providers: [ providers: [
], ],
exports: [ exports: [
AddonModBookIndexComponent, AddonModBookIndexComponent
AddonModBookTocPopoverComponent
], ],
entryComponents: [ entryComponents: [
AddonModBookIndexComponent, AddonModBookIndexComponent
AddonModBookTocPopoverComponent
] ]
}) })
export class AddonModBookComponentsModule {} export class AddonModBookComponentsModule {}

View File

@ -1,11 +1,12 @@
<!-- Buttons to add to the header. --> <!-- Buttons to add to the header. -->
<core-navbar-buttons end> <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> <ion-icon name="bookmark"></ion-icon>
</button> </button>
<core-context-menu> <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="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="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 [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="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> <core-context-menu-item *ngIf="size" [priority]="500" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item>

View File

@ -12,14 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Optional, Injector } from '@angular/core'; import { Component, Optional, Injector, Input } from '@angular/core';
import { Content, PopoverController } from 'ionic-angular'; import { Content, ModalController } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component';
import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book'; import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book';
import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler'; import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler';
import { AddonModBookTocPopoverComponent } from '../../components/toc-popover/toc-popover';
/** /**
* Component that displays a book. * Component that displays a book.
@ -29,6 +28,8 @@ import { AddonModBookTocPopoverComponent } from '../../components/toc-popover/to
templateUrl: 'addon-mod-book-index.html', templateUrl: 'addon-mod-book-index.html',
}) })
export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent { export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent {
@Input() initialChapterId: string; // The initial chapter ID to load.
component = AddonModBookProvider.COMPONENT; component = AddonModBookProvider.COMPONENT;
chapterContent: string; chapterContent: string;
previousChapter: string; previousChapter: string;
@ -40,7 +41,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider, constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider,
private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler, private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler,
private popoverCtrl: PopoverController, @Optional() private content: Content) { private modalCtrl: ModalController, @Optional() private content: Content) {
super(injector); super(injector);
} }
@ -59,15 +60,23 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
* @param {MouseEvent} event Event. * @param {MouseEvent} event Event.
*/ */
showToc(event: MouseEvent): void { showToc(event: MouseEvent): void {
const popover = this.popoverCtrl.create(AddonModBookTocPopoverComponent, { // Create the toc modal.
chapters: this.chapters 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' });
popover.onDidDismiss((chapterId) => { modal.onDidDismiss((chapterId) => {
if (chapterId) {
this.changeChapter(chapterId); this.changeChapter(chapterId);
}
}); });
popover.present({ modal.present({
ev: event ev: event
}); });
} }
@ -128,7 +137,19 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
this.contentsMap = this.bookProvider.getContentsMap(this.module.contents); this.contentsMap = this.bookProvider.getContentsMap(this.module.contents);
this.chapters = this.bookProvider.getTocList(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') { if (typeof this.currentChapter == 'undefined') {
// Load the first chapter.
this.currentChapter = this.bookProvider.getFirstChapter(this.chapters); this.currentChapter = this.bookProvider.getFirstChapter(this.chapters);
} }

View File

@ -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>

View File

@ -1,4 +1,5 @@
{ {
"errorchapter": "Error reading chapter of book.", "errorchapter": "Error reading chapter of book.",
"modulenameplural": "Books" "modulenameplural": "Books",
"toc": "Table of contents"
} }

View File

@ -12,5 +12,5 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </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> </ion-content>

View File

@ -30,10 +30,12 @@ export class AddonModBookIndexPage {
title: string; title: string;
module: any; module: any;
courseId: number; courseId: number;
chapterId: number;
constructor(navParams: NavParams) { constructor(navParams: NavParams) {
this.module = navParams.get('module') || {}; this.module = navParams.get('module') || {};
this.courseId = navParams.get('courseId'); this.courseId = navParams.get('courseId');
this.chapterId = navParams.get('chapterId');
this.title = this.module.name; this.title = this.module.name;
} }

View File

@ -0,0 +1,19 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.mod_book.toc' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<nav>
<ion-list>
<a ion-item text-wrap *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)" [class.core-nav-item-selected]="selected == chapter.id">
<p [attr.padding-left]="chapter.level == 1 ? true : null">{{chapter.title}}</p>
</a>
</ion-list>
</nav>
</ion-content>

View File

@ -0,0 +1,31 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModBookTocPage } from './toc';
@NgModule({
declarations: [
AddonModBookTocPage,
],
imports: [
CoreDirectivesModule,
IonicPageModule.forChild(AddonModBookTocPage),
TranslateModule.forChild()
],
})
export class AddonModBookTocPageModule {}

View File

@ -13,21 +13,24 @@
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { NavParams, ViewController } from 'ionic-angular'; import { IonicPage, NavParams, ViewController } from 'ionic-angular';
import { AddonModBookTocChapter } from '../../providers/book'; 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({ @Component({
selector: 'addon-mod-book-toc-popover', selector: 'page-addon-mod-book-toc',
templateUrl: 'addon-mod-assign-submission-toc-popover.html' templateUrl: 'toc.html'
}) })
export class AddonModBookTocPopoverComponent { export class AddonModBookTocPage {
chapters: AddonModBookTocChapter[]; chapters: AddonModBookTocChapter[];
selected: number;
constructor(navParams: NavParams, private viewCtrl: ViewController) { constructor(navParams: NavParams, private viewCtrl: ViewController) {
this.chapters = navParams.get('chapters') || []; this.chapters = navParams.get('chapters') || [];
this.selected = navParams.get('selected');
} }
/** /**
@ -38,4 +41,11 @@ export class AddonModBookTocPopoverComponent {
loadChapter(id: string): void { loadChapter(id: string): void {
this.viewCtrl.dismiss(id); this.viewCtrl.dismiss(id);
} }
/**
* Close modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
}
} }

View File

@ -22,6 +22,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
/** /**
* A book chapter inside the toc list. * A book chapter inside the toc list.
@ -64,7 +65,8 @@ export class AddonModBookProvider {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, private http: Http, 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'); this.logger = logger.getInstance('AddonModBookProvider');
} }
@ -378,14 +380,15 @@ export class AddonModBookProvider {
* *
* @param {number} id Module ID. * @param {number} id Module ID.
* @param {string} chapterId Chapter 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. * @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 = { const params = {
bookid: id, bookid: id,
chapterid: chapterId 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);
} }
} }

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler';
import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
/** /**
* Handler to treat links to book. * Handler to treat links to book.
@ -26,4 +27,27 @@ export class AddonModBookLinkHandler extends CoreContentLinksModuleIndexHandler
constructor(courseHelper: CoreCourseHelperProvider) { constructor(courseHelper: CoreCourseHelperProvider) {
super(courseHelper, 'AddonModBook', 'book'); 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);
}
}];
}
} }

View File

@ -65,8 +65,12 @@ export class AddonModBookModuleHandler implements CoreCourseModuleHandler {
title: module.name, title: module.name,
class: 'addon-mod_book-handler', class: 'addon-mod_book-handler',
showDownloadButton: true, showDownloadButton: true,
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void {
navCtrl.push('AddonModBookIndexPage', {module: module, courseId: courseId}, options); const pageParams = {module: module, courseId: courseId};
if (params) {
Object.assign(pageParams, params);
}
navCtrl.push('AddonModBookIndexPage', pageParams, options);
} }
}; };
} }

View File

@ -15,11 +15,13 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-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 { AddonModChatComponentsModule } from './components/components.module';
import { AddonModChatProvider } from './providers/chat'; import { AddonModChatProvider } from './providers/chat';
import { AddonModChatLinkHandler } from './providers/link-handler'; import { AddonModChatLinkHandler } from './providers/link-handler';
import { AddonModChatListLinkHandler } from './providers/list-link-handler'; import { AddonModChatListLinkHandler } from './providers/list-link-handler';
import { AddonModChatModuleHandler } from './providers/module-handler'; import { AddonModChatModuleHandler } from './providers/module-handler';
import { AddonModChatPrefetchHandler } from './providers/prefetch-handler';
// List of providers (without handlers). // List of providers (without handlers).
export const ADDON_MOD_CHAT_PROVIDERS: any[] = [ export const ADDON_MOD_CHAT_PROVIDERS: any[] = [
@ -37,15 +39,18 @@ export const ADDON_MOD_CHAT_PROVIDERS: any[] = [
AddonModChatLinkHandler, AddonModChatLinkHandler,
AddonModChatListLinkHandler, AddonModChatListLinkHandler,
AddonModChatModuleHandler, AddonModChatModuleHandler,
AddonModChatPrefetchHandler
] ]
}) })
export class AddonModChatModule { export class AddonModChatModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChatModuleHandler, constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChatModuleHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModChatLinkHandler, contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModChatLinkHandler,
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModChatPrefetchHandler,
listLinkHandler: AddonModChatListLinkHandler) { listLinkHandler: AddonModChatListLinkHandler) {
moduleDelegate.registerHandler(moduleHandler); moduleDelegate.registerHandler(moduleHandler);
contentLinksDelegate.registerHandler(linkHandler); contentLinksDelegate.registerHandler(linkHandler);
contentLinksDelegate.registerHandler(listLinkHandler); contentLinksDelegate.registerHandler(listLinkHandler);
prefetchDelegate.registerHandler(prefetchHandler);
} }
} }

View File

@ -3,7 +3,9 @@
<core-context-menu> <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="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="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="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-context-menu>
</core-navbar-buttons> </core-navbar-buttons>
@ -16,7 +18,8 @@
<ion-icon name="time"></ion-icon> {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }} <ion-icon name="time"></ion-icon> {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }}
</ion-card> </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="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> </div>
</core-loading> </core-loading>

View File

@ -33,9 +33,10 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
chatInfo: any; chatInfo: any;
protected title: string; protected title: string;
protected sessionsAvailable = false;
constructor(injector: Injector, private chatProvider: AddonModChatProvider, private timeUtils: CoreTimeUtilsProvider, constructor(injector: Injector, private chatProvider: AddonModChatProvider, private timeUtils: CoreTimeUtilsProvider,
private navCtrl: NavController) { protected navCtrl: NavController) {
super(injector); super(injector);
} }
@ -83,6 +84,10 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
// All data obtained, now fill the context menu. // All data obtained, now fill the context menu.
this.fillContextMenu(refresh); 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; const title = this.chat.name || this.moduleName;
this.navCtrl.push('AddonModChatChatPage', {chatId: this.chat.id, courseId: this.courseId, title: title }); 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});
}
} }

View File

@ -1,5 +1,6 @@
{ {
"beep": "Beep", "beep": "Beep",
"chatreport": "Chat sessions",
"currentusers": "Current users", "currentusers": "Current users",
"enterchat": "Click here to enter the chat now", "enterchat": "Click here to enter the chat now",
"entermessage": "Enter your message", "entermessage": "Enter your message",
@ -11,10 +12,14 @@
"messagebeepsyou": "{{$a}} has just beeped you!", "messagebeepsyou": "{{$a}} has just beeped you!",
"messageenter": "{{$a}} has just entered this chat", "messageenter": "{{$a}} has just entered this chat",
"messageexit": "{{$a}} has left this chat", "messageexit": "{{$a}} has left this chat",
"messages": "Messages",
"modulenameplural": "Chats", "modulenameplural": "Chats",
"mustbeonlinetosendmessages": "You must be online to send messages.", "mustbeonlinetosendmessages": "You must be online to send messages.",
"nomessages": "No messages yet", "nomessages": "No messages yet",
"nosessionsfound": "No sessions found",
"send": "Send", "send": "Send",
"sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)", "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"
} }

View File

@ -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>

View File

@ -0,0 +1,37 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { AddonModChatComponentsModule } from '../../components/components.module';
import { AddonModChatSessionMessagesPage } from './session-messages';
@NgModule({
declarations: [
AddonModChatSessionMessagesPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
AddonModChatComponentsModule,
IonicPageModule.forChild(AddonModChatSessionMessagesPage),
TranslateModule.forChild()
],
})
export class AddonModChatSessionMessagesPageModule {}

View File

@ -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;
}
}

View File

@ -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');
}
}

View File

@ -0,0 +1,45 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.mod_chat.chatreport' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<core-split-view>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshSessions($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-item text-wrap *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
<ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
<ion-select [(ngModel)]="groupId" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-groupslabel" interface="action-sheet">
<ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label id="addon-chat-showalllabel">{{ 'addon.mod_chat.showincompletesessions' | translate }}</ion-label>
<ion-toggle [(ngModel)]="showAll" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-showalllabel"></ion-toggle>
</ion-item>
<ion-card *ngFor="let session of sessions" (click)="openSession(session)"
[class.addon-mod-chat-session-selected]="session.sessionstart == selectedSessionStart && groupId == selectedSessionGroupId"
[class.addon-mod-chat-session-show-more]="session.sessionusers.length < session.allsessionusers.length">
<ion-item text-wrap>
<h2>{{ session.sessionstart * 1000 | coreFormatDate }}</h2>
<p *ngIf="session.duration">{{ session.duration | coreDuration }}</p>
</ion-item>
<ion-card-content>
<p *ngFor="let user of session.sessionusers">
{{ user.userfullname }} <span *ngIf="user.messagecount">({{ user.messagecount }})</span>
</p>
</ion-card-content>
<div *ngIf="session.sessionusers.length < session.allsessionusers.length">
<button ion-button clear (click)="showMoreUsers(session, $event)">
{{ 'core.showmore' | translate }}
</button>
</div>
</ion-card>
<core-empty-box *ngIf="sessions.length == 0" icon="chatbubbles" [message]="'addon.mod_chat.nosessionsfound' | translate">
</core-empty-box>
</core-loading>
</ion-content>
</core-split-view>

View File

@ -0,0 +1,37 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { AddonModChatComponentsModule } from '../../components/components.module';
import { AddonModChatSessionsPage } from './sessions';
@NgModule({
declarations: [
AddonModChatSessionsPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
AddonModChatComponentsModule,
IonicPageModule.forChild(AddonModChatSessionsPage),
TranslateModule.forChild()
],
})
export class AddonModChatSessionsPageModule {}

View File

@ -0,0 +1,8 @@
ion-app.app-root page-addon-mod-chat-sessions {
.addon-mod-chat-session-show-more .card-content{
padding-bottom: 0;
}
.addon-mod-chat-session-selected {
border-top: 5px solid $core-splitview-selected;
}
}

View File

@ -0,0 +1,165 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreUserProvider } from '@core/user/providers/user';
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { AddonModChatProvider } from '../../providers/chat';
/**
* Page that displays list of chat sessions.
*/
@IonicPage({ segment: 'addon-mod-chat-sessions' })
@Component({
selector: 'page-addon-mod-chat-sessions',
templateUrl: 'sessions.html',
})
export class AddonModChatSessionsPage {
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
protected courseId: number;
protected cmId: number;
protected chatId: number;
protected loaded = false;
protected showAll = false;
protected groupId = 0;
protected groupInfo: CoreGroupInfo;
protected sessions = [];
protected selectedSessionStart: number;
protected selectedSessionGroupId: number;
constructor(navParams: NavParams, private chatProvider: AddonModChatProvider, private domUtils: CoreDomUtilsProvider,
private userProvider: CoreUserProvider, private groupsProvider: CoreGroupsProvider,
private translate: TranslateService, private utils: CoreUtilsProvider) {
this.courseId = navParams.get('courseId');
this.cmId = navParams.get('cmId');
this.chatId = navParams.get('chatId');
this.fetchSessions().then(() => {
if (this.splitviewCtrl.isOn() && this.sessions.length > 0) {
this.openSession(this.sessions[0]);
}
});
}
/**
* Fetch chat sessions.
*
* @param {number} [showLoading] Display a loading modal.
* @return {Promise<any>} Promise resolved when done.
*/
fetchSessions(showLoading?: boolean): Promise<any> {
const modal = showLoading ? this.domUtils.showModalLoading() : null;
return this.groupsProvider.getActivityGroupInfo(this.cmId, false).then((groupInfo) => {
this.groupInfo = groupInfo;
if (groupInfo.groups && groupInfo.groups.length > 0) {
if (!groupInfo.groups.find((group) => group.id === this.groupId)) {
this.groupId = groupInfo.groups[0].id;
}
} else {
this.groupId = 0;
}
return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll);
}).then((sessions) => {
// Fetch user profiles.
const promises = [];
sessions.forEach((session) => {
session.duration = session.sessionend - session.sessionstart;
session.sessionusers.forEach((sessionUser) => {
if (!sessionUser.userfullname) {
// The WS does not return the user name, fetch user profile.
promises.push(this.userProvider.getProfile(sessionUser.userid, this.courseId, true).then((user) => {
sessionUser.userfullname = user.fullname;
}).catch(() => {
// Error getting profile, most probably the user is deleted.
sessionUser.userfullname = this.translate.instant('core.deleteduser') + ' ' + sessionUser.userid;
}));
}
});
// If session has more than 4 users we display a "Show more" link.
session.allsessionusers = session.sessionusers;
if (session.sessionusers.length > 4) {
session.sessionusers = session.allsessionusers.slice(0, 3);
}
});
return Promise.all(promises).then(() => {
this.sessions = sessions;
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true);
}).finally(() => {
this.loaded = true;
modal && modal.dismiss();
});
}
/**
* Refresh chat sessions.
*
* @param {any} refresher Refresher.
*/
refreshSessions(refresher: any): void {
const promises = [
this.groupsProvider.invalidateActivityGroupInfo(this.cmId),
this.chatProvider.invalidateSessions(this.chatId, this.groupId, this.showAll)
];
this.utils.allPromises(promises).finally(() => {
this.fetchSessions().finally(() => {
refresher.complete();
});
});
}
/**
* Navigate to a session.
*
* @param {any} session Chat session.
*/
openSession(session: any): void {
this.selectedSessionStart = session.sessionstart;
this.selectedSessionGroupId = this.groupId;
const params = {
courseId: this.courseId,
chatId: this.chatId,
groupId: this.groupId,
sessionStart: session.sessionstart,
sessionEnd: session.sessionend
};
this.splitviewCtrl.push('AddonModChatSessionMessagesPage', params);
}
/**
* Show more session users.
*
* @param {any} session Chat session.
* @param {Event} $event The event.
*/
showMoreUsers(session: any, $event: Event): void {
session.sessionusers = session.allsessionusers;
$event.stopPropagation();
}
}

View File

@ -13,8 +13,12 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUserProvider } from '@core/user/providers/user';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSiteWSPreSets } from '@classes/site';
/** /**
* Service that provides some features for chats. * Service that provides some features for chats.
@ -24,25 +28,29 @@ export class AddonModChatProvider {
static COMPONENT = 'mmaModChat'; static COMPONENT = 'mmaModChat';
static POLL_INTERVAL = 4000; 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. * Get a chat.
* *
* @param {number} courseId Course ID. * @param {number} courseId Course ID.
* @param {number} cmId Course module ID. * @param {number} cmId Course module ID.
* @param {boolean} [refresh=false] True when we should not get the value from the cache. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the chat is retrieved. * @return {Promise<any>} Promise resolved when the chat is retrieved.
*/ */
getChat(courseId: number, cmId: number, refresh: boolean = false): Promise<any> { getChat(courseId: number, cmId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = { const params = {
courseids: [courseId] courseids: [courseId]
}; };
const preSets = { const preSets: CoreSiteWSPreSets = {
getFromCache: refresh ? false : undefined, cacheKey: this.getChatsCacheKey(courseId)
}; };
return this.sitesProvider.getCurrentSite().read('mod_chat_get_chats_by_courses', params, preSets).then((response) => { return site.read('mod_chat_get_chats_by_courses', params, preSets).then((response) => {
if (response.chats) { if (response.chats) {
const chat = response.chats.find((chat) => chat.coursemodule == cmId); const chat = response.chats.find((chat) => chat.coursemodule == cmId);
if (chat) { if (chat) {
@ -52,6 +60,7 @@ export class AddonModChatProvider {
return Promise.reject(null); return Promise.reject(null);
}); });
});
} }
/** /**
@ -77,15 +86,16 @@ export class AddonModChatProvider {
/** /**
* Report a chat as being viewed. * Report a chat as being viewed.
* *
* @param {number} chatId Chat instance ID. * @param {number} id Chat instance ID.
* @return {Promise<any>} Promise resolved when the WS call is executed. * @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 = { 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.userfullname = user.fullname;
message.userprofileimageurl = user.profileimageurl; message.userprofileimageurl = user.profileimageurl;
}).catch(() => { }).catch(() => {
// Error getting profile. Set default data. // Error getting profile, most probably the user is deleted.
message.userfullname = message.userid; 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 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 + ':';
}
} }

View File

@ -18,6 +18,7 @@ import { AddonModChatIndexComponent } from '../components/index/index';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { AddonModChatProvider } from './chat';
/** /**
* Handler to support chat modules. * Handler to support chat modules.
@ -38,7 +39,7 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler {
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true [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. * 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. * @return {CoreCourseModuleHandlerData} Data to render the module.
*/ */
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
return { const data: CoreCourseModuleHandlerData = {
icon: this.courseProvider.getModuleIconSrc(this.modName, module.modicon), icon: this.courseProvider.getModuleIconSrc(this.modName, module.modicon),
title: module.name, title: module.name,
class: 'addon-mod_chat-handler', class: 'addon-mod_chat-handler',
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void {
navCtrl.push('AddonModChatIndexPage', {module: module, courseId: courseId}, options); 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;
} }
/** /**

View File

@ -0,0 +1,185 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonModChatProvider } from './chat';
/**
* Handler to prefetch chats.
*/
@Injectable()
export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandlerBase {
name = 'AddonModChat';
modName = 'chat';
component = AddonModChatProvider.COMPONENT;
constructor(translate: TranslateService,
appProvider: CoreAppProvider,
utils: CoreUtilsProvider,
courseProvider: CoreCourseProvider,
filepoolProvider: CoreFilepoolProvider,
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
private groupsProvider: CoreGroupsProvider,
private userProvider: CoreUserProvider,
private chatProvider: AddonModChatProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils);
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
isEnabled(): boolean | Promise<boolean> {
return this.chatProvider.areSessionsAvailable();
}
/**
* Invalidate the prefetched content.
*
* @param {number} moduleId The module ID.
* @param {number} courseId The course ID the module belongs to.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number): Promise<any> {
return this.chatProvider.getChat(courseId, moduleId).then((chat) => {
const promises = [
this.chatProvider.invalidateAllSessions(chat.id),
this.chatProvider.invalidateAllSessionMessages(chat.id)
];
return this.utils.allPromises(promises);
});
}
/**
* Invalidate WS calls needed to determine module status (usually, to check if module is downloadable).
* It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data.
*
* @param {any} module Module.
* @param {number} courseId Course ID the module belongs to.
* @return {Promise<any>} Promise resolved when invalidated.
*/
invalidateModule(module: any, courseId: number): Promise<any> {
const promises = [
this.chatProvider.invalidateChats(courseId),
this.courseProvider.invalidateModule(module.id)
];
return this.utils.allPromises(promises);
}
/**
* Prefetch a module.
*
* @param {any} module Module.
* @param {number} courseId Course ID the module belongs to.
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
* @param {string} [dirPath] Path of the directory where to store all the content files.
* @return {Promise<any>} Promise resolved when done.
*/
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
return this.prefetchPackage(module, courseId, single, this.prefetchChat.bind(this));
}
/**
* Prefetch a chat.
*
* @param {any} module The module object returned by WS.
* @param {number} courseId Course ID the module belongs to.
* @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section.
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved when done.
*/
protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
// Prefetch chat and group info.
const promises = [
this.chatProvider.getChat(courseId, module.id, siteId),
this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId)
];
return Promise.all(promises).then(([chat, groupInfo]: [any, CoreGroupInfo]) => {
const promises = [];
let groupIds = [0];
if (groupInfo.groups && groupInfo.groups.length > 0) {
groupIds = groupInfo.groups.map((group) => group.id);
}
groupIds.forEach((groupId) => {
// Prefetch complete sessions.
promises.push(this.chatProvider.getSessions(chat.id, groupId, false, true, siteId).catch((error) => {
// Ignore group error.
if (error.errorcode != 'notingroup') {
return Promise.reject(error);
}
}));
// Prefetch all sessions.
promises.push(this.chatProvider.getSessions(chat.id, groupId, true, true, siteId).then((sessions) => {
const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, siteId));
return Promise.all(promises);
}).catch((error) => {
// Ignore group error.
if (error.errorcode != 'notingroup') {
return Promise.reject(error);
}
}));
});
return Promise.all(promises);
});
}
/**
* Prefetch chat session messages and user profiles.
*
* @param {number} chatId Chat ID.
* @param {any} session Session object.
* @param {number} groupId Group ID.
* @param {number} courseId Course ID the module belongs to.
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved when done.
*/
protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise<any> {
return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId)
.then((messages) => {
const users = {};
session.sessionusers.forEach((user) => {
users[user.userid] = true;
});
messages.forEach((message) => {
users[message.userid] = true;
});
const userIds = Object.keys(users).map(Number);
return this.userProvider.prefetchProfiles(userIds, courseId, siteId).catch(() => {
// Ignore errors, some users might not exist.
});
});
}
}

Some files were not shown because too many files have changed in this diff Show More