commit
7ef37cbebf
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
Dockerfile
|
|
@ -0,0 +1,8 @@
|
||||||
|
# This file has been retrieved from angular repository.
|
||||||
|
|
||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# JS and TS files must always use LF for tools to work
|
||||||
|
*.js eol=lf
|
||||||
|
*.ts eol=lf
|
15
.travis.yml
15
.travis.yml
|
@ -1,16 +1,23 @@
|
||||||
sudo: required
|
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
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# This image is based on the fat node 11 image.
|
||||||
|
# We require fat images as neither alpine, or slim, include git binaries.
|
||||||
|
FROM node:11
|
||||||
|
|
||||||
|
# Port 8100 for ionic dev server.
|
||||||
|
EXPOSE 8100
|
||||||
|
|
||||||
|
# Port 35729 is the live-reload server.
|
||||||
|
EXPOSE 35729
|
||||||
|
|
||||||
|
# Port 53703 is the Chrome dev logger port.
|
||||||
|
EXPOSE 53703
|
||||||
|
|
||||||
|
# MoodleMobile uses Cordova, Ionic, and Gulp.
|
||||||
|
RUN npm install -g cordova ionic gulp && rm -rf /root/.npm
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# The setup script will handle npm installation, cordova setup, and gulp setup.
|
||||||
|
RUN npm run setup && rm -rf /root/.npm
|
||||||
|
|
||||||
|
# Provide a Healthcheck command for easier use in CI.
|
||||||
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s CMD curl -f http://localhost:8100 || exit 1
|
||||||
|
|
||||||
|
CMD ["ionic", "serve", "-b"]
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>AD_UNIT_ID_FOR_BANNER_TEST</key>
|
||||||
|
<string>ca-app-pub-3940256099942544/2934735716</string>
|
||||||
|
<key>AD_UNIT_ID_FOR_INTERSTITIAL_TEST</key>
|
||||||
|
<string>ca-app-pub-3940256099942544/4411468910</string>
|
||||||
|
<key>CLIENT_ID</key>
|
||||||
|
<string>694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr.apps.googleusercontent.com</string>
|
||||||
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
|
<string>com.googleusercontent.apps.694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr</string>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyA-77ZjkxII6GV97CC9rdUl83rzdEXu_rM</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>694767596569</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.moodle.moodlemobile</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>moodlemobile-push</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>moodlemobile-push.appspot.com</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:694767596569:ios:a4cdad4d168c9d1a</string>
|
||||||
|
<key>DATABASE_URL</key>
|
||||||
|
<string>https://moodlemobile-push.firebaseio.com</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
29
config.xml
29
config.xml
|
@ -1,5 +1,5 @@
|
||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?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" />
|
||||||
|
|
|
@ -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}}/'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "694767596569",
|
||||||
|
"firebase_url": "https://moodlemobile-push.firebaseio.com",
|
||||||
|
"project_id": "moodlemobile-push",
|
||||||
|
"storage_bucket": "moodlemobile-push.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:694767596569:android:a4cdad4d168c9d1a",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.moodle.moodlemobile"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "694767596569-icveqqa2n56oh44l6ev1dr2oh67nh8il.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyCb2zogu0P_aZ2LNgdwzshWExITPKTXJyk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyDRT1HwT0gSsTty0whOVtoNKAh8SPrJXLE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"analytics_service": {
|
||||||
|
"status": 1
|
||||||
|
},
|
||||||
|
"appinvite_service": {
|
||||||
|
"status": 1,
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
},
|
||||||
|
"ads_service": {
|
||||||
|
"status": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
10
gulpfile.js
10
gulpfile.js
|
@ -47,7 +47,13 @@ function treatFile(file, data) {
|
||||||
return; // ignore
|
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();
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// This hook copies Android splash screen files from dev directories into the appropriate platform specific location.
|
|
||||||
// The code was extracted from here: http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/
|
|
||||||
|
|
||||||
var filesToCopy = [{
|
|
||||||
'resources/android/splash/drawable-land-hdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-hdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-land-ldpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-ldpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-land-mdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-mdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-land-xhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-land-xxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-land-xxxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-port-hdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-hdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-port-ldpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-ldpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-port-mdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-mdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-port-xhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-port-xxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png'
|
|
||||||
}, {
|
|
||||||
'resources/android/splash/drawable-port-xxxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
// no need to configure below
|
|
||||||
var rootDir = process.argv[2];
|
|
||||||
|
|
||||||
filesToCopy.forEach(function(obj) {
|
|
||||||
Object.keys(obj).forEach(function(key) {
|
|
||||||
var val = obj[key];
|
|
||||||
var srcFile = path.join(rootDir, key);
|
|
||||||
var destFile = path.join(rootDir, val);
|
|
||||||
var destDir = path.dirname(destFile);
|
|
||||||
if (fs.existsSync(srcFile) && fs.existsSync(destDir)) {
|
|
||||||
fs.createReadStream(srcFile).pipe(fs.createWriteStream(destFile));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "integration" != "${SOURCE_BRANCH}" ]
|
||||||
|
then
|
||||||
|
# A space-separated list of additional tags to place on this image.
|
||||||
|
additionalTags=(latest)
|
||||||
|
|
||||||
|
# Tag and push image for each additional tag
|
||||||
|
for tag in ${additionalTags[@]}; do
|
||||||
|
echo "Tagging {$IMAGE_NAME} as ${DOCKER_REPO}:${tag}"
|
||||||
|
docker tag $IMAGE_NAME ${DOCKER_REPO}:${tag}
|
||||||
|
|
||||||
|
echo "Pushing ${DOCKER_REPO}:${tag}"
|
||||||
|
docker push ${DOCKER_REPO}:${tag}
|
||||||
|
done
|
||||||
|
fi
|
|
@ -6,6 +6,5 @@
|
||||||
},
|
},
|
||||||
"type": "ionic-angular",
|
"type": "ionic-angular",
|
||||||
"watchPatterns": [],
|
"watchPatterns": [],
|
||||||
"pro_id": "com.moodle.moodlemobile",
|
|
||||||
"id": "com.moodle.moodlemobile"
|
"id": "com.moodle.moodlemobile"
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "moodlemobile",
|
"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 |
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Script for generating the Desktop builds
|
||||||
|
#
|
||||||
|
|
||||||
|
sudo apt-get install -y libnss3-dev
|
||||||
|
|
||||||
|
npm install -g electron-builder electron
|
||||||
|
|
||||||
|
electron-builder install-app-deps
|
||||||
|
|
||||||
|
jq -s '.[0] + {"name": "moodledesktop"}' package.json > package_new.json
|
||||||
|
mv package_new.json package.json
|
||||||
|
|
||||||
|
rm -Rf desktop/dist
|
||||||
|
|
||||||
|
npm run desktop.dist -- -l --x64 --ia32
|
||||||
|
|
||||||
|
if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] ; then
|
||||||
|
git clone -q https://$GIT_TOKEN@github.com/moodlemobile/bma-apps-builds.git ../apps
|
||||||
|
|
||||||
|
mv desktop/dist/*.AppImage ../apps
|
||||||
|
|
||||||
|
cd ../apps
|
||||||
|
|
||||||
|
chmod +x *.AppImage
|
||||||
|
mv *i386.AppImage linux-ia32.AppImage
|
||||||
|
mv Moodle*.AppImage linux-x64.AppImage
|
||||||
|
ls
|
||||||
|
|
||||||
|
git add .
|
||||||
|
git commit -m "Linux desktop versions from Travis build $TRAVIS_BUILD_NUMBER"
|
||||||
|
git push
|
||||||
|
fi
|
|
@ -19,7 +19,7 @@ import { CoreLoginHelperProvider } from '@core/login/providers/helper';
|
||||||
import { AddonBadgesProvider } from './badges';
|
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 {
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
|
||||||
|
import { CoreUserDelegate } from '@core/user/providers/user-delegate';
|
||||||
|
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
|
||||||
|
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { AddonBlogProvider } from './providers/blog';
|
||||||
|
import { AddonBlogMainMenuHandler } from './providers/mainmenu-handler';
|
||||||
|
import { AddonBlogUserHandler } from './providers/user-handler';
|
||||||
|
import { AddonBlogCourseOptionHandler } from './providers/course-option-handler';
|
||||||
|
import { AddonBlogComponentsModule } from './components/components.module';
|
||||||
|
import { AddonBlogIndexLinkHandler } from './providers/index-link-handler';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AddonBlogComponentsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonBlogProvider,
|
||||||
|
AddonBlogMainMenuHandler,
|
||||||
|
AddonBlogUserHandler,
|
||||||
|
AddonBlogCourseOptionHandler,
|
||||||
|
AddonBlogIndexLinkHandler
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonBlogModule {
|
||||||
|
constructor(mainMenuDelegate: CoreMainMenuDelegate, menuHandler: AddonBlogMainMenuHandler,
|
||||||
|
userHandler: AddonBlogUserHandler, userDelegate: CoreUserDelegate,
|
||||||
|
courseOptionHandler: AddonBlogCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate,
|
||||||
|
linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate) {
|
||||||
|
mainMenuDelegate.registerHandler(menuHandler);
|
||||||
|
userDelegate.registerHandler(userHandler);
|
||||||
|
courseOptionsDelegate.registerHandler(courseOptionHandler);
|
||||||
|
contentLinksDelegate.registerHandler(linkHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
|
||||||
|
import { AddonBlogEntriesComponent } from './entries/entries';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonBlogEntriesComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CorePipesModule,
|
||||||
|
CoreCommentsComponentsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonBlogEntriesComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonBlogEntriesComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonBlogComponentsModule {}
|
|
@ -0,0 +1,49 @@
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="loaded" (ionRefresh)="refresh($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||||
|
<div class="safe-padding-horizontal">
|
||||||
|
<ion-item *ngIf="showMyIssuesToggle">
|
||||||
|
<ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label>
|
||||||
|
<ion-toggle [(ngModel)]="onlyMyEntries"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
<core-empty-box *ngIf="entries && entries.length == 0" icon="fa-newspaper-o" [message]="'addon.blog.noentriesyet' | translate"></core-empty-box>
|
||||||
|
<ng-container *ngFor="let entry of entries">
|
||||||
|
<ion-card *ngIf="!onlyMyEntries || entry.userid == currentUserId">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<ion-avatar core-user-avatar [user]="entry.user" item-start [courseId]="entry.courseid"></ion-avatar>
|
||||||
|
<h2>
|
||||||
|
<core-format-text [text]="entry.subject"></core-format-text>
|
||||||
|
<ion-note float-end padding-left text-end>
|
||||||
|
{{ 'addon.blog.' + entry.publishTranslated | translate}}
|
||||||
|
</ion-note>
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<ion-note float-end padding-left text-end>
|
||||||
|
{{entry.created | coreDateDayOrTime}}
|
||||||
|
</ion-note>
|
||||||
|
{{entry.user && entry.user.fullname}}
|
||||||
|
</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-card-content>
|
||||||
|
<core-format-text [text]="entry.summary" [component]="this.component" [componentId]="entry.id"></core-format-text>
|
||||||
|
<ion-item>
|
||||||
|
<core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid" contextLevel="user"></core-comments>
|
||||||
|
</ion-item>
|
||||||
|
<core-file *ngFor="let file of entry.attachmentfiles" [file]="file" [component]="this.component" [componentId]="entry.id"></core-file>
|
||||||
|
<a ion-item *ngIf="entry.uniquehash" [href]="entry.uniquehash" core-link>{{ 'addon.blog.linktooriginalentry' | translate }}</a>
|
||||||
|
</ion-card-content>
|
||||||
|
<ion-row text-center>
|
||||||
|
<ion-col *ngIf="entry.lastmodified > entry.created">
|
||||||
|
<ion-note>
|
||||||
|
<ion-icon name="time"></ion-icon> {{entry.lastmodified | coreTimeAgo}}
|
||||||
|
</ion-note>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-card>
|
||||||
|
</ng-container>
|
||||||
|
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,176 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, Input, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { Content } from 'ionic-angular';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { AddonBlogProvider } from '../../providers/blog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays the blog entries.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-blog-entries',
|
||||||
|
templateUrl: 'addon-blog-entries.html',
|
||||||
|
})
|
||||||
|
export class AddonBlogEntriesComponent implements OnInit {
|
||||||
|
@Input() userId?: number;
|
||||||
|
@Input() courseId?: number;
|
||||||
|
@Input() cmId?: number;
|
||||||
|
@Input() entryId?: number;
|
||||||
|
@Input() groupId?: number;
|
||||||
|
@Input() tagId?: number;
|
||||||
|
|
||||||
|
protected filter = {};
|
||||||
|
protected pageLoaded = 0;
|
||||||
|
|
||||||
|
@ViewChild(Content) content: Content;
|
||||||
|
|
||||||
|
loaded = false;
|
||||||
|
canLoadMore = false;
|
||||||
|
loadMoreError = false;
|
||||||
|
entries = [];
|
||||||
|
currentUserId: number;
|
||||||
|
showMyIssuesToggle = false;
|
||||||
|
onlyMyEntries = false;
|
||||||
|
component = AddonBlogProvider.COMPONENT;
|
||||||
|
|
||||||
|
constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider,
|
||||||
|
protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider) {
|
||||||
|
this.currentUserId = sitesProvider.getCurrentSiteUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.userId) {
|
||||||
|
this.filter['userid'] = this.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.courseId) {
|
||||||
|
this.filter['courseid'] = this.courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cmId) {
|
||||||
|
this.filter['cmid'] = this.cmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.entryId) {
|
||||||
|
this.filter['entryid'] = this.entryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.groupId) {
|
||||||
|
this.filter['groupid'] = this.groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tagId) {
|
||||||
|
this.filter['tagid'] = this.tagId;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchEntries().then(() => {
|
||||||
|
this.blogProvider.logView(this.filter).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch blog entries.
|
||||||
|
*
|
||||||
|
* @param {boolean} [refresh] Empty events array first.
|
||||||
|
* @return {Promise<any>} Promise with the entries.
|
||||||
|
*/
|
||||||
|
private fetchEntries(refresh: boolean = false): Promise<any> {
|
||||||
|
this.loadMoreError = false;
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
this.pageLoaded = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.blogProvider.getEntries(this.filter, this.pageLoaded).then((result) => {
|
||||||
|
const promises = result.entries.map((entry) => {
|
||||||
|
switch (entry.publishstate) {
|
||||||
|
case 'draft':
|
||||||
|
entry.publishTranslated = 'publishtonoone';
|
||||||
|
break;
|
||||||
|
case 'site':
|
||||||
|
entry.publishTranslated = 'publishtosite';
|
||||||
|
break;
|
||||||
|
case 'public':
|
||||||
|
entry.publishTranslated = 'publishtoworld';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
entry.publishTranslated = 'privacy:unknown';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userProvider.getProfile(entry.userid, entry.courseid, true).then((user) => {
|
||||||
|
entry.user = user;
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
this.showMyIssuesToggle = false;
|
||||||
|
this.entries = result.entries;
|
||||||
|
} else {
|
||||||
|
this.entries = this.entries.concat(result.entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canLoadMore = result.totalentries > this.entries.length;
|
||||||
|
this.pageLoaded++;
|
||||||
|
|
||||||
|
this.showMyIssuesToggle = !this.userId;
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'addon.blog.errorloadentries', true);
|
||||||
|
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||||
|
}).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to load more entries.
|
||||||
|
*
|
||||||
|
* @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading.
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
loadMore(infiniteComplete?: any): Promise<any> {
|
||||||
|
return this.fetchEntries().finally(() => {
|
||||||
|
infiniteComplete && infiniteComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh blog entries on PTR.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher instance.
|
||||||
|
*/
|
||||||
|
refresh(refresher?: any): void {
|
||||||
|
this.blogProvider.invalidateEntries(this.filter).finally(() => {
|
||||||
|
this.fetchEntries(true).finally(() => {
|
||||||
|
if (refresher) {
|
||||||
|
refresher.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"blog": "Blog",
|
||||||
|
"blogentries": "Blog entries",
|
||||||
|
"errorloadentries": "Error loading blog entries.",
|
||||||
|
"linktooriginalentry": "Link to original blog entry",
|
||||||
|
"noentriesyet": "No visible entries here",
|
||||||
|
"publishtonoone": "Yourself (draft)",
|
||||||
|
"publishtosite": "Anyone on this site",
|
||||||
|
"publishtoworld": "Anyone in the world",
|
||||||
|
"showonlyyourentries": "Show only your entries",
|
||||||
|
"siteblogheading": "Site blog"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar core-back-button>
|
||||||
|
<ion-title>{{ title | translate }}</ion-title>
|
||||||
|
<ion-buttons end></ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<addon-blog-entries class="core-avoid-header" [courseId]="courseId" [userId]="userId" [cmId]="cmId" [entryId]="entryId" [groupId]="groupId" [tagId]="tagId"></addon-blog-entries>
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonBlogEntriesPage } from './entries';
|
||||||
|
import { AddonBlogComponentsModule } from '../../components/components.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonBlogEntriesPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
AddonBlogComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonBlogEntriesPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonBlogEntriesPageModule {}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the list of blog entries.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-blog-entries' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-blog-entries',
|
||||||
|
templateUrl: 'entries.html',
|
||||||
|
})
|
||||||
|
export class AddonBlogEntriesPage {
|
||||||
|
userId: number;
|
||||||
|
courseId: number;
|
||||||
|
cmId: number;
|
||||||
|
entryId: number;
|
||||||
|
groupId: number;
|
||||||
|
tagId: number;
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
constructor(params: NavParams) {
|
||||||
|
this.userId = params.get('userId');
|
||||||
|
this.courseId = params.get('courseId');
|
||||||
|
this.cmId = params.get('cmId');
|
||||||
|
this.entryId = params.get('entryId');
|
||||||
|
this.groupId = params.get('groupId');
|
||||||
|
this.tagId = params.get('tagId');
|
||||||
|
|
||||||
|
if (!this.userId && !this.courseId && !this.cmId && !this.entryId && !this.groupId && !this.tagId) {
|
||||||
|
this.title = 'addon.blog.siteblogheading';
|
||||||
|
} else {
|
||||||
|
this.title = 'addon.blog.blogentries';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle blog entries.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonBlogProvider {
|
||||||
|
static ENTRIES_PER_PAGE = 10;
|
||||||
|
static COMPONENT = 'blog';
|
||||||
|
protected ROOT_CACHE_KEY = 'addonBlog:';
|
||||||
|
protected logger;
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected utils: CoreUtilsProvider) {
|
||||||
|
this.logger = logger.getInstance('AddonBlogProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the blog plugin is enabled for a certain site.
|
||||||
|
*
|
||||||
|
* This method is called quite often and thus should only perform a quick
|
||||||
|
* check, we should not be calling WS from here.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false or rejected otherwise.
|
||||||
|
*/
|
||||||
|
isPluginEnabled(siteId?: string): Promise<boolean> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.wsAvailable('core_blog_get_entries') &&
|
||||||
|
site.canUseAdvancedFeature('enableblogs');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cache key for the blog entries.
|
||||||
|
*
|
||||||
|
* @param {any} [filter] Filter to apply on search.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
getEntriesCacheKey(filter: any = {}): string {
|
||||||
|
return this.ROOT_CACHE_KEY + this.utils.sortAndStringify(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blog entries.
|
||||||
|
*
|
||||||
|
* @param {any} [filter] Filter to apply on search.
|
||||||
|
* @param {any} [page=0] Page of the blog entries to fetch.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise to be resolved when the entries are retrieved.
|
||||||
|
*/
|
||||||
|
getEntries(filter: any = {}, page: number = 0, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const data = {
|
||||||
|
filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value'),
|
||||||
|
page: page,
|
||||||
|
perpage: AddonBlogProvider.ENTRIES_PER_PAGE
|
||||||
|
};
|
||||||
|
|
||||||
|
const preSets = {
|
||||||
|
cacheKey: this.getEntriesCacheKey(filter)
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.read('core_blog_get_entries', data, preSets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate blog entries WS call.
|
||||||
|
*
|
||||||
|
* @param {any} [filter] Filter to apply on search
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateEntries(filter: any = {}, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.invalidateWsCacheForKey(this.getEntriesCacheKey(filter));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger the blog_entries_viewed event.
|
||||||
|
*
|
||||||
|
* @param {any} [filter] Filter to apply on search.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise to be resolved when done.
|
||||||
|
*/
|
||||||
|
logView(filter: any = {}, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const data = {
|
||||||
|
filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value')
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.write('core_blog_view_entries', data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable, Injector } from '@angular/core';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||||
|
import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate';
|
||||||
|
import { CoreCoursesProvider } from '@core/courses/providers/courses';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||||
|
import { AddonBlogEntriesComponent } from '../components/entries/entries';
|
||||||
|
import { AddonBlogProvider } from './blog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Course nav handler.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonBlogCourseOptionHandler implements CoreCourseOptionsHandler {
|
||||||
|
name = 'AddonBlog';
|
||||||
|
priority = 100;
|
||||||
|
|
||||||
|
constructor(protected coursesProvider: CoreCoursesProvider, protected blogProvider: AddonBlogProvider,
|
||||||
|
protected courseHelper: CoreCourseHelperProvider, protected courseProvider: CoreCourseProvider,
|
||||||
|
protected sitesProvider: CoreSitesProvider, protected filepoolProvider: CoreFilepoolProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should invalidate the data to determine if the handler is enabled for a certain course.
|
||||||
|
*
|
||||||
|
* @param {number} courseId The course ID.
|
||||||
|
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||||
|
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise<any> {
|
||||||
|
return this.courseProvider.invalidateCourseBlocks(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return this.blogProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled for a certain course.
|
||||||
|
*
|
||||||
|
* @param {number} courseId The course ID.
|
||||||
|
* @param {any} accessData Access type and data. Default, guest, ...
|
||||||
|
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||||
|
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
||||||
|
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
|
||||||
|
return this.courseHelper.hasABlockNamed(courseId, 'blog_menu').then((enabled) => {
|
||||||
|
if (enabled && navOptions && typeof navOptions.blogs != 'undefined') {
|
||||||
|
return navOptions.blogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data needed to render the handler.
|
||||||
|
*
|
||||||
|
* @param {Injector} injector Injector.
|
||||||
|
* @param {number} courseId The course ID.
|
||||||
|
* @return {CoreCourseOptionsHandlerData|Promise<CoreCourseOptionsHandlerData>} Data or promise resolved with the data.
|
||||||
|
*/
|
||||||
|
getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> {
|
||||||
|
return {
|
||||||
|
title: 'addon.blog.blog',
|
||||||
|
class: 'addon-blog-handler',
|
||||||
|
component: AddonBlogEntriesComponent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
|
||||||
|
*
|
||||||
|
* @param {any} course The course.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
prefetch(course: any): Promise<any> {
|
||||||
|
const siteId = this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
return this.blogProvider.getEntries({courseid: course.id}).then((result) => {
|
||||||
|
return result.entries.map((entry) => {
|
||||||
|
let files = [];
|
||||||
|
|
||||||
|
if (entry.attachmentfiles && entry.attachmentfiles.length) {
|
||||||
|
files = entry.attachmentfiles;
|
||||||
|
}
|
||||||
|
if (entry.summaryfiles && entry.summaryfiles.length) {
|
||||||
|
files = files.concat(entry.summaryfiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
return this.filepoolProvider.addFilesToQueue(siteId, files, entry.module, entry.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
|
||||||
|
import { AddonBlogProvider } from './blog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to blog page.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonBlogIndexLinkHandler extends CoreContentLinksHandlerBase {
|
||||||
|
name = 'AddonBlogIndexLinkHandler';
|
||||||
|
featureName = 'CoreUserDelegate_AddonBlog';
|
||||||
|
pattern = /\/blog\/index\.php/;
|
||||||
|
|
||||||
|
constructor(private blogProvider: AddonBlogProvider, private loginHelper: CoreLoginHelperProvider) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of actions for a link (url).
|
||||||
|
*
|
||||||
|
* @param {string[]} siteIds List of sites the URL belongs to.
|
||||||
|
* @param {string} url The URL to treat.
|
||||||
|
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||||
|
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
|
||||||
|
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
|
||||||
|
*/
|
||||||
|
getActions(siteIds: string[], url: string, params: any, courseId?: number):
|
||||||
|
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||||
|
const pageParams: any = {};
|
||||||
|
|
||||||
|
params.userid ? pageParams['userId'] = parseInt(params.userid, 10) : null;
|
||||||
|
params.modid ? pageParams['cmId'] = parseInt(params.modid, 10) : null;
|
||||||
|
params.courseid ? pageParams['courseId'] = parseInt(params.courseid, 10) : null;
|
||||||
|
params.entryid ? pageParams['entryId'] = parseInt(params.entryid, 10) : null;
|
||||||
|
params.groupid ? pageParams['groupId'] = parseInt(params.groupid, 10) : null;
|
||||||
|
params.tagid ? pageParams['tagId'] = parseInt(params.tagid, 10) : null;
|
||||||
|
|
||||||
|
return [{
|
||||||
|
action: (siteId, navCtrl?): void => {
|
||||||
|
// Always use redirect to make it the new history root (to avoid "loops" in history).
|
||||||
|
this.loginHelper.redirect('AddonBlogEntriesPage', pageParams, siteId);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled for a certain site (site + user) and a URL.
|
||||||
|
* If not defined, defaults to true.
|
||||||
|
*
|
||||||
|
* @param {string} siteId The site ID.
|
||||||
|
* @param {string} url The URL to treat.
|
||||||
|
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||||
|
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
|
||||||
|
*/
|
||||||
|
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
|
||||||
|
|
||||||
|
return this.blogProvider.isPluginEnabled(siteId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AddonBlogProvider } from './blog';
|
||||||
|
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to inject an option into main menu.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonBlogMainMenuHandler implements CoreMainMenuHandler {
|
||||||
|
name = 'AddonBlog';
|
||||||
|
priority = 450;
|
||||||
|
|
||||||
|
constructor(private blogProvider: AddonBlogProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return this.blogProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data needed to render the handler.
|
||||||
|
*
|
||||||
|
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreMainMenuHandlerData {
|
||||||
|
return {
|
||||||
|
icon: 'fa-newspaper-o',
|
||||||
|
title: 'addon.blog.siteblogheading',
|
||||||
|
page: 'AddonBlogEntriesPage',
|
||||||
|
class: 'addon-blog-handler'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate';
|
||||||
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
|
import { AddonBlogProvider } from './blog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile item handler.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonBlogUserHandler implements CoreUserProfileHandler {
|
||||||
|
name = 'AddonBlog:blogs';
|
||||||
|
priority = 300;
|
||||||
|
type = CoreUserDelegate.TYPE_NEW_PAGE;
|
||||||
|
|
||||||
|
constructor(protected linkHelper: CoreContentLinksHelperProvider, protected blogProvider: AddonBlogProvider) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return this.blogProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if handler is enabled for this user in this context.
|
||||||
|
*
|
||||||
|
* @param {any} user User to check.
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||||
|
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
||||||
|
* @return {boolean|Promise<boolean>} Promise resolved with true if enabled, resolved with false otherwise.
|
||||||
|
*/
|
||||||
|
isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data needed to render the handler.
|
||||||
|
*
|
||||||
|
* @return {CoreUserProfileHandlerData} Data needed to render the handler.
|
||||||
|
*/
|
||||||
|
getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData {
|
||||||
|
return {
|
||||||
|
icon: 'fa-newspaper-o',
|
||||||
|
title: 'addon.blog.blogentries',
|
||||||
|
class: 'addon-blog-handler',
|
||||||
|
action: (event, navCtrl, user, courseId): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
// Always use redirect to make it the new history root (to avoid "loops" in history).
|
||||||
|
this.linkHelper.goInSite(navCtrl, 'AddonBlogEntriesPage', { userId: user.id, courseId: courseId });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,7 +71,7 @@ export class AddonCalendarModule {
|
||||||
newName: AddonCalendarProvider.EVENTS_TABLE,
|
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.
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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}}"
|
||||||
}
|
}
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"pluginname": "Online text submissions"
|
"pluginname": "Online text submissions",
|
||||||
|
"wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again."
|
||||||
}
|
}
|
|
@ -14,6 +14,7 @@
|
||||||
// limitations under the License.
|
// 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);
|
||||||
|
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<ion-list>
|
|
||||||
<a ion-item text-wrap *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)" detail-none>
|
|
||||||
<p [attr.padding-left]="chapter.level == 1 ? true : null">{{chapter.title}}</p>
|
|
||||||
</a>
|
|
||||||
</ion-list>
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"errorchapter": "Error reading chapter of book.",
|
"errorchapter": "Error reading chapter of book.",
|
||||||
"modulenameplural": "Books"
|
"modulenameplural": "Books",
|
||||||
|
"toc": "Table of contents"
|
||||||
}
|
}
|
|
@ -12,5 +12,5 @@
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher-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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar core-back-button>
|
||||||
|
<ion-title>{{ 'addon.mod_book.toc' | translate }}</ion-title>
|
||||||
|
<ion-buttons end>
|
||||||
|
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||||
|
<ion-icon name="close"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<nav>
|
||||||
|
<ion-list>
|
||||||
|
<a ion-item text-wrap *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)" [class.core-nav-item-selected]="selected == chapter.id">
|
||||||
|
<p [attr.padding-left]="chapter.level == 1 ? true : null">{{chapter.title}}</p>
|
||||||
|
</a>
|
||||||
|
</ion-list>
|
||||||
|
</nav>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,31 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModBookTocPage } from './toc';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModBookTocPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
IonicPageModule.forChild(AddonModBookTocPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModBookTocPageModule {}
|
|
@ -13,21 +13,24 @@
|
||||||
// limitations under the License.
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar core-back-button>
|
||||||
|
<ion-title>{{ 'addon.mod_chat.messages' | translate }}</ion-title>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshMessages($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<div *ngFor="let message of messages; index as index; last as last">
|
||||||
|
<div text-center *ngIf="showDate(messages[index], messages[index - 1])" class="addon-mod-chat-notice">
|
||||||
|
<ion-badge text-wrap color="light">
|
||||||
|
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimedayshort" }}</span>
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div text-center *ngIf="message.issystem && message.message == 'enter'" class="addon-mod-chat-notice">
|
||||||
|
<ion-badge text-wrap color="light">
|
||||||
|
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }}</span>
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div text-center *ngIf="message.issystem && message.message == 'exit'" class="addon-mod-chat-notice">
|
||||||
|
<ion-badge text-wrap color="light">
|
||||||
|
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }}</span>
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-item text-wrap *ngIf="!message.issystem && message.message.substr(0, 4) != 'beep'" class="addon-mod-chat-message">
|
||||||
|
<ion-avatar core-user-avatar [user]="message" item-start></ion-avatar>
|
||||||
|
<h2>
|
||||||
|
<p float-end>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }}</p>
|
||||||
|
<core-format-text [text]="message.userfullname"></core-format-text>
|
||||||
|
</h2>
|
||||||
|
<core-format-text [text]="message.message"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,37 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { AddonModChatComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModChatSessionMessagesPage } from './session-messages';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModChatSessionMessagesPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CorePipesModule,
|
||||||
|
AddonModChatComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModChatSessionMessagesPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModChatSessionMessagesPageModule {}
|
|
@ -0,0 +1,9 @@
|
||||||
|
ion-app.app-root page-addon-mod-chat-session-messages {
|
||||||
|
.addon-mod-chat-notice {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.addon-mod-chat-message {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { AddonModChatProvider } from '../../providers/chat';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays list of chat session messages.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-chat-session-messages' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-chat-session-messages',
|
||||||
|
templateUrl: 'session-messages.html',
|
||||||
|
})
|
||||||
|
export class AddonModChatSessionMessagesPage {
|
||||||
|
|
||||||
|
protected courseId: number;
|
||||||
|
protected chatId: number;
|
||||||
|
protected sessionStart: number;
|
||||||
|
protected sessionEnd: number;
|
||||||
|
protected groupId: number;
|
||||||
|
protected loaded = false;
|
||||||
|
protected messages = [];
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private chatProvider: AddonModChatProvider) {
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.chatId = navParams.get('chatId');
|
||||||
|
this.groupId = navParams.get('groupId');
|
||||||
|
this.sessionStart = navParams.get('sessionStart');
|
||||||
|
this.sessionEnd = navParams.get('sessionEnd');
|
||||||
|
|
||||||
|
this.fetchMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch session messages.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchMessages(): Promise<any> {
|
||||||
|
return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId)
|
||||||
|
.then((messages) => {
|
||||||
|
return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => {
|
||||||
|
this.messages = messages;
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh session messages.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshMessages(refresher: any): void {
|
||||||
|
this.chatProvider.invalidateSessionMessages(this.chatId, this.sessionStart, this.groupId).finally(() => {
|
||||||
|
this.fetchMessages().finally(() => {
|
||||||
|
refresher.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the date should be displayed between messages (when the day changes at midnight for example).
|
||||||
|
*
|
||||||
|
* @param {any} message New message object.
|
||||||
|
* @param {any} prevMessage Previous message object.
|
||||||
|
* @return {boolean} True if messages are from diferent days, false othetwise.
|
||||||
|
*/
|
||||||
|
showDate(message: any, prevMessage: any): boolean {
|
||||||
|
if (!prevMessage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if day has changed.
|
||||||
|
return !moment(message.timestamp * 1000).isSame(prevMessage.timestamp * 1000, 'day');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar core-back-button>
|
||||||
|
<ion-title>{{ 'addon.mod_chat.chatreport' | translate }}</ion-title>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<core-split-view>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshSessions($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<ion-item text-wrap *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
|
||||||
|
<ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
|
||||||
|
<ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
|
||||||
|
<ion-select [(ngModel)]="groupId" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-groupslabel" interface="action-sheet">
|
||||||
|
<ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label id="addon-chat-showalllabel">{{ 'addon.mod_chat.showincompletesessions' | translate }}</ion-label>
|
||||||
|
<ion-toggle [(ngModel)]="showAll" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-showalllabel"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
<ion-card *ngFor="let session of sessions" (click)="openSession(session)"
|
||||||
|
[class.addon-mod-chat-session-selected]="session.sessionstart == selectedSessionStart && groupId == selectedSessionGroupId"
|
||||||
|
[class.addon-mod-chat-session-show-more]="session.sessionusers.length < session.allsessionusers.length">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ session.sessionstart * 1000 | coreFormatDate }}</h2>
|
||||||
|
<p *ngIf="session.duration">{{ session.duration | coreDuration }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-card-content>
|
||||||
|
<p *ngFor="let user of session.sessionusers">
|
||||||
|
{{ user.userfullname }} <span *ngIf="user.messagecount">({{ user.messagecount }})</span>
|
||||||
|
</p>
|
||||||
|
</ion-card-content>
|
||||||
|
<div *ngIf="session.sessionusers.length < session.allsessionusers.length">
|
||||||
|
<button ion-button clear (click)="showMoreUsers(session, $event)">
|
||||||
|
{{ 'core.showmore' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ion-card>
|
||||||
|
<core-empty-box *ngIf="sessions.length == 0" icon="chatbubbles" [message]="'addon.mod_chat.nosessionsfound' | translate">
|
||||||
|
</core-empty-box>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
||||||
|
</core-split-view>
|
|
@ -0,0 +1,37 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { AddonModChatComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModChatSessionsPage } from './sessions';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModChatSessionsPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CorePipesModule,
|
||||||
|
AddonModChatComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModChatSessionsPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModChatSessionsPageModule {}
|
|
@ -0,0 +1,8 @@
|
||||||
|
ion-app.app-root page-addon-mod-chat-sessions {
|
||||||
|
.addon-mod-chat-session-show-more .card-content{
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.addon-mod-chat-session-selected {
|
||||||
|
border-top: 5px solid $core-splitview-selected;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, ViewChild } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { AddonModChatProvider } from '../../providers/chat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays list of chat sessions.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-chat-sessions' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-chat-sessions',
|
||||||
|
templateUrl: 'sessions.html',
|
||||||
|
})
|
||||||
|
export class AddonModChatSessionsPage {
|
||||||
|
|
||||||
|
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||||
|
|
||||||
|
protected courseId: number;
|
||||||
|
protected cmId: number;
|
||||||
|
protected chatId: number;
|
||||||
|
protected loaded = false;
|
||||||
|
protected showAll = false;
|
||||||
|
protected groupId = 0;
|
||||||
|
protected groupInfo: CoreGroupInfo;
|
||||||
|
protected sessions = [];
|
||||||
|
protected selectedSessionStart: number;
|
||||||
|
protected selectedSessionGroupId: number;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, private chatProvider: AddonModChatProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
|
private userProvider: CoreUserProvider, private groupsProvider: CoreGroupsProvider,
|
||||||
|
private translate: TranslateService, private utils: CoreUtilsProvider) {
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.cmId = navParams.get('cmId');
|
||||||
|
this.chatId = navParams.get('chatId');
|
||||||
|
|
||||||
|
this.fetchSessions().then(() => {
|
||||||
|
if (this.splitviewCtrl.isOn() && this.sessions.length > 0) {
|
||||||
|
this.openSession(this.sessions[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch chat sessions.
|
||||||
|
*
|
||||||
|
* @param {number} [showLoading] Display a loading modal.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
fetchSessions(showLoading?: boolean): Promise<any> {
|
||||||
|
const modal = showLoading ? this.domUtils.showModalLoading() : null;
|
||||||
|
|
||||||
|
return this.groupsProvider.getActivityGroupInfo(this.cmId, false).then((groupInfo) => {
|
||||||
|
this.groupInfo = groupInfo;
|
||||||
|
|
||||||
|
if (groupInfo.groups && groupInfo.groups.length > 0) {
|
||||||
|
if (!groupInfo.groups.find((group) => group.id === this.groupId)) {
|
||||||
|
this.groupId = groupInfo.groups[0].id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.groupId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll);
|
||||||
|
}).then((sessions) => {
|
||||||
|
// Fetch user profiles.
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
sessions.forEach((session) => {
|
||||||
|
session.duration = session.sessionend - session.sessionstart;
|
||||||
|
session.sessionusers.forEach((sessionUser) => {
|
||||||
|
if (!sessionUser.userfullname) {
|
||||||
|
// The WS does not return the user name, fetch user profile.
|
||||||
|
promises.push(this.userProvider.getProfile(sessionUser.userid, this.courseId, true).then((user) => {
|
||||||
|
sessionUser.userfullname = user.fullname;
|
||||||
|
}).catch(() => {
|
||||||
|
// Error getting profile, most probably the user is deleted.
|
||||||
|
sessionUser.userfullname = this.translate.instant('core.deleteduser') + ' ' + sessionUser.userid;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If session has more than 4 users we display a "Show more" link.
|
||||||
|
session.allsessionusers = session.sessionusers;
|
||||||
|
if (session.sessionusers.length > 4) {
|
||||||
|
session.sessionusers = session.allsessionusers.slice(0, 3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
this.sessions = sessions;
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
modal && modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh chat sessions.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshSessions(refresher: any): void {
|
||||||
|
const promises = [
|
||||||
|
this.groupsProvider.invalidateActivityGroupInfo(this.cmId),
|
||||||
|
this.chatProvider.invalidateSessions(this.chatId, this.groupId, this.showAll)
|
||||||
|
];
|
||||||
|
|
||||||
|
this.utils.allPromises(promises).finally(() => {
|
||||||
|
this.fetchSessions().finally(() => {
|
||||||
|
refresher.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a session.
|
||||||
|
*
|
||||||
|
* @param {any} session Chat session.
|
||||||
|
*/
|
||||||
|
openSession(session: any): void {
|
||||||
|
this.selectedSessionStart = session.sessionstart;
|
||||||
|
this.selectedSessionGroupId = this.groupId;
|
||||||
|
const params = {
|
||||||
|
courseId: this.courseId,
|
||||||
|
chatId: this.chatId,
|
||||||
|
groupId: this.groupId,
|
||||||
|
sessionStart: session.sessionstart,
|
||||||
|
sessionEnd: session.sessionend
|
||||||
|
};
|
||||||
|
this.splitviewCtrl.push('AddonModChatSessionMessagesPage', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show more session users.
|
||||||
|
*
|
||||||
|
* @param {any} session Chat session.
|
||||||
|
* @param {Event} $event The event.
|
||||||
|
*/
|
||||||
|
showMoreUsers(session: any, $event: Event): void {
|
||||||
|
session.sessionusers = session.allsessionusers;
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,12 @@
|
||||||
// limitations under the License.
|
// 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 + ':';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||||
|
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { AddonModChatProvider } from './chat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to prefetch chats.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandlerBase {
|
||||||
|
name = 'AddonModChat';
|
||||||
|
modName = 'chat';
|
||||||
|
component = AddonModChatProvider.COMPONENT;
|
||||||
|
|
||||||
|
constructor(translate: TranslateService,
|
||||||
|
appProvider: CoreAppProvider,
|
||||||
|
utils: CoreUtilsProvider,
|
||||||
|
courseProvider: CoreCourseProvider,
|
||||||
|
filepoolProvider: CoreFilepoolProvider,
|
||||||
|
sitesProvider: CoreSitesProvider,
|
||||||
|
domUtils: CoreDomUtilsProvider,
|
||||||
|
private groupsProvider: CoreGroupsProvider,
|
||||||
|
private userProvider: CoreUserProvider,
|
||||||
|
private chatProvider: AddonModChatProvider) {
|
||||||
|
|
||||||
|
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return this.chatProvider.areSessionsAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param {number} moduleId The module ID.
|
||||||
|
* @param {number} courseId The course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||||
|
return this.chatProvider.getChat(courseId, moduleId).then((chat) => {
|
||||||
|
const promises = [
|
||||||
|
this.chatProvider.invalidateAllSessions(chat.id),
|
||||||
|
this.chatProvider.invalidateAllSessionMessages(chat.id)
|
||||||
|
];
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate WS calls needed to determine module status (usually, to check if module is downloadable).
|
||||||
|
* It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when invalidated.
|
||||||
|
*/
|
||||||
|
invalidateModule(module: any, courseId: number): Promise<any> {
|
||||||
|
const promises = [
|
||||||
|
this.chatProvider.invalidateChats(courseId),
|
||||||
|
this.courseProvider.invalidateModule(module.id)
|
||||||
|
];
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param {string} [dirPath] Path of the directory where to store all the content files.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
|
||||||
|
return this.prefetchPackage(module, courseId, single, this.prefetchChat.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a chat.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object returned by WS.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||||
|
// Prefetch chat and group info.
|
||||||
|
const promises = [
|
||||||
|
this.chatProvider.getChat(courseId, module.id, siteId),
|
||||||
|
this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId)
|
||||||
|
];
|
||||||
|
|
||||||
|
return Promise.all(promises).then(([chat, groupInfo]: [any, CoreGroupInfo]) => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
let groupIds = [0];
|
||||||
|
if (groupInfo.groups && groupInfo.groups.length > 0) {
|
||||||
|
groupIds = groupInfo.groups.map((group) => group.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIds.forEach((groupId) => {
|
||||||
|
// Prefetch complete sessions.
|
||||||
|
promises.push(this.chatProvider.getSessions(chat.id, groupId, false, true, siteId).catch((error) => {
|
||||||
|
// Ignore group error.
|
||||||
|
if (error.errorcode != 'notingroup') {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Prefetch all sessions.
|
||||||
|
promises.push(this.chatProvider.getSessions(chat.id, groupId, true, true, siteId).then((sessions) => {
|
||||||
|
const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, siteId));
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).catch((error) => {
|
||||||
|
// Ignore group error.
|
||||||
|
if (error.errorcode != 'notingroup') {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch chat session messages and user profiles.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @param {any} session Session object.
|
||||||
|
* @param {number} groupId Group ID.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise<any> {
|
||||||
|
return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId)
|
||||||
|
.then((messages) => {
|
||||||
|
const users = {};
|
||||||
|
session.sessionusers.forEach((user) => {
|
||||||
|
users[user.userid] = true;
|
||||||
|
});
|
||||||
|
messages.forEach((message) => {
|
||||||
|
users[message.userid] = true;
|
||||||
|
});
|
||||||
|
const userIds = Object.keys(users).map(Number);
|
||||||
|
|
||||||
|
return this.userProvider.prefetchProfiles(userIds, courseId, siteId).catch(() => {
|
||||||
|
// Ignore errors, some users might not exist.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue