commit
5e220c4d03
|
@ -46,3 +46,4 @@ e2e/build
|
|||
!/desktop/assets/
|
||||
!/desktop/electron.js
|
||||
src/configconstants.ts
|
||||
.moodleapp-dev-config
|
||||
|
|
|
@ -52,7 +52,7 @@ jobs:
|
|||
script: scripts/aot.sh
|
||||
- stage: build
|
||||
name: "Build Android"
|
||||
if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND NOT branch = desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank))
|
||||
if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch != desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank))
|
||||
os: linux
|
||||
dist: trusty
|
||||
group: edge
|
||||
|
@ -69,9 +69,9 @@ jobs:
|
|||
script: scripts/aot.sh
|
||||
- stage: build
|
||||
name: "Build iOS"
|
||||
if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND NOT branch = desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank))
|
||||
if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch != desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank))
|
||||
os: osx
|
||||
osx_image: xcode11.3
|
||||
osx_image: xcode12u
|
||||
env:
|
||||
- BUILD_PLATFORM='ios'
|
||||
script: scripts/aot.sh
|
||||
|
@ -88,7 +88,7 @@ jobs:
|
|||
name: "Build MacOS"
|
||||
if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch = desktop) OR (env(DEPLOY) = 3 AND tag IS NOT blank))
|
||||
os: osx
|
||||
osx_image: xcode11.3
|
||||
osx_image: xcode12u
|
||||
env:
|
||||
- ELECTRON_CACHE=$HOME/.cache/electron
|
||||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
||||
|
|
|
@ -13,3 +13,5 @@ jszip has problems with "lie" dependency on greater versions than 3.1
|
|||
|
||||
promise.prototype.finally has problems on greater versions than 3.1
|
||||
|
||||
cordova-ios: should remain on 5.1 because of: https://github.com/apache/cordova-ios/pull/801
|
||||
|
||||
|
|
19
config.xml
19
config.xml
|
@ -1,5 +1,5 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget android-versionCode="39200" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.2.0" version="3.9.2" versionCode="39200" 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 android-versionCode="39300" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.3.0" version="3.9.3" versionCode="39300" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<name>Moodle</name>
|
||||
<description>Moodle official app</description>
|
||||
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
|
||||
|
@ -56,7 +56,7 @@
|
|||
<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" />
|
||||
<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" />
|
||||
</edit-config>
|
||||
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application">
|
||||
<application android:largeHeap="true" android:usesCleartextTraffic="true" />
|
||||
|
@ -112,11 +112,6 @@
|
|||
<param name="android-package" value="io.github.pwlin.cordova.plugins.fileopener2.FileOpener2" />
|
||||
</feature>
|
||||
</config-file>
|
||||
<config-file parent="/manifest/application" target="AndroidManifest.xml">
|
||||
<provider android:authorities="${applicationId}.opener.provider" android:exported="false" android:grantUriPermissions="true" android:name="io.github.pwlin.cordova.plugins.fileopener2.FileProvider">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/opener_paths" />
|
||||
</provider>
|
||||
</config-file>
|
||||
<config-file parent="/*" target="res/xml/config.xml">
|
||||
<feature name="FileTransfer">
|
||||
<param name="android-package" value="org.apache.cordova.filetransfer.FileTransfer" />
|
||||
|
@ -219,6 +214,14 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
</config-file>
|
||||
<config-file parent="/*" target="res/xml/config.xml">
|
||||
<feature name="Media">
|
||||
<param name="android-package" value="org.apache.cordova.media.AudioHandler" />
|
||||
</feature>
|
||||
</config-file>
|
||||
<config-file parent="/*" target="AndroidManifest.xml">
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
||||
</config-file>
|
||||
</platform>
|
||||
<platform name="ios">
|
||||
<resource-file src="GoogleService-Info.plist" />
|
||||
|
@ -241,7 +244,7 @@
|
|||
<true />
|
||||
</edit-config>
|
||||
<edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString">
|
||||
<string>3.9.2</string>
|
||||
<string>3.9.3</string>
|
||||
</edit-config>
|
||||
<config-file parent="FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED" target="*-Info.plist">
|
||||
<string>YES</string>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?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>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,17 +1,26 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Script to sign macOSX pkg.
|
||||
# https://www.electronjs.org/docs/tutorial/mac-app-store-submission-guide
|
||||
#
|
||||
|
||||
# Name of your app.
|
||||
APP="Moodle Desktop"
|
||||
# The path of your app to sign.
|
||||
APP_PATH="desktop/dist/mas/Moodle Desktop.app"
|
||||
# The path to the location you want to put the signed package.
|
||||
RESULT_PATH="desktop/dist/mas/$APP.pkg"
|
||||
# The name of certificates you requested.
|
||||
APP_KEY="3rd Party Mac Developer Application: Moodle Pty Ltd (2NU57U5PAW)"
|
||||
INSTALLER_KEY="3rd Party Mac Developer Installer: Moodle Pty Ltd (2NU57U5PAW)"
|
||||
|
||||
|
||||
BASEPATH="desktop/dist/mas"
|
||||
# The path of your app to sign.
|
||||
APP_PATH="${BASEPATH}/${APP}.app"
|
||||
# The path to the location you want to put the signed package.
|
||||
RESULT_PATH="${BASEPATH}/${APP}.pkg"
|
||||
|
||||
# The path of your plist files.
|
||||
CHILD_PLIST="desktop/assets/mac/child.plist"
|
||||
PARENT_PLIST="desktop/assets/mac/parent.plist"
|
||||
LOGINHELPER_PLIST="desktop/assets/mac/loginhelper.plist"
|
||||
|
||||
FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks"
|
||||
|
||||
|
@ -21,10 +30,8 @@ codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electr
|
|||
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/Contents/MacOS/$APP Helper"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/Contents/MacOS/$APP Helper EH"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper NP.app/Contents/MacOS/$APP Helper NP"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper NP.app/"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/Contents/MacOS/$APP Login Helper"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP"
|
||||
codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH"
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<Identity Name="3312ADB7.MoodleDesktop"
|
||||
ProcessorArchitecture="x64"
|
||||
Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6"
|
||||
Version="3.9.2.0" />
|
||||
Version="3.9.3.0" />
|
||||
<Properties>
|
||||
<DisplayName>Moodle Desktop</DisplayName>
|
||||
<PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName>
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const DEV_CONFIG_FILE = '.moodleapp-dev-config';
|
||||
|
||||
/**
|
||||
* Class to read and write dev-config data from a file.
|
||||
*/
|
||||
class DevConfig {
|
||||
|
||||
constructor() {
|
||||
this.loadFileData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a setting.
|
||||
*
|
||||
* @param name Name of the setting to get.
|
||||
* @param defaultValue Value to use if not found.
|
||||
*/
|
||||
get(name, defaultValue) {
|
||||
return typeof this.config[name] != 'undefined' ? this.config[name] : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load file data to memory.
|
||||
*/
|
||||
loadFileData() {
|
||||
if (!fs.existsSync(DEV_CONFIG_FILE)) {
|
||||
this.config = {};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.config = JSON.parse(fs.readFileSync(DEV_CONFIG_FILE));
|
||||
} catch (error) {
|
||||
console.error('Error reading dev config file.', error);
|
||||
this.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save some settings.
|
||||
*
|
||||
* @param settings Object with the settings to save.
|
||||
*/
|
||||
save(settings) {
|
||||
this.config = Object.assign(this.config, settings);
|
||||
|
||||
// Save the data in the dev file.
|
||||
fs.writeFileSync(DEV_CONFIG_FILE, JSON.stringify(this.config, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new DevConfig();
|
|
@ -0,0 +1,237 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const exec = require('child_process').exec;
|
||||
const fs = require('fs');
|
||||
const DevConfig = require('./dev-config');
|
||||
const Utils = require('./utils');
|
||||
|
||||
/**
|
||||
* Class to run git commands.
|
||||
*/
|
||||
class Git {
|
||||
|
||||
/**
|
||||
* Create a patch.
|
||||
*
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param saveTo Path to the file to save the patch to. If not defined, the patch contents will be returned.
|
||||
* @return Promise resolved when done. If saveTo not provided, it will return the patch contents.
|
||||
*/
|
||||
createPatch(range, saveTo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`git format-patch ${range} --stdout`, (err, result) => {
|
||||
if (err) {
|
||||
reject(err || 'Cannot create patch.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!saveTo) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save it to a file.
|
||||
const directory = saveTo.substring(0, saveTo.lastIndexOf('/'));
|
||||
if (directory && directory != '.' && directory != '..' && !fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory);
|
||||
}
|
||||
fs.writeFileSync(saveTo, result);
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current branch.
|
||||
*
|
||||
* @return Promise resolved with the branch name.
|
||||
*/
|
||||
getCurrentBranch() {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('git branch --show-current', (err, branch) => {
|
||||
if (branch) {
|
||||
resolve(branch.replace('\n', ''));
|
||||
} else {
|
||||
reject (err || 'Current branch not found.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HEAD commit for a certain branch.
|
||||
*
|
||||
* @param branch Name of the branch.
|
||||
* @param branchData Parsed branch data. If not provided it will be calculated.
|
||||
* @return HEAD commit.
|
||||
*/
|
||||
async getHeadCommit(branch, branchData) {
|
||||
if (!branchData) {
|
||||
// Parse the branch to get the project and issue number.
|
||||
branchData = Utils.parseBranch(branch);
|
||||
}
|
||||
|
||||
// Loop over the last commits to find the first commit messages that doesn't belong to the issue.
|
||||
const commitsString = await this.log(50, branch, '%s_____%H');
|
||||
const commits = commitsString.split('\n');
|
||||
commits.pop(); // Remove last element, it's an empty string.
|
||||
|
||||
for (let i = 0; i < commits.length; i++) {
|
||||
const commit = commits[i];
|
||||
const match = Utils.getIssueFromCommitMessage(commit) == branchData.issue;
|
||||
|
||||
if (i === 0 && !match) {
|
||||
// Most recent commit doesn't belong to the issue. Stop looking.
|
||||
break;
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
// The commit does not match any more, we found it!
|
||||
return commit.split('_____')[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find the commit using the commit names, get the last commit in the integration branch.
|
||||
const remote = DevConfig.get('upstreamRemote', 'origin');
|
||||
console.log(`Head commit not found using commit messages. Get last commit from ${remote}/integration`);
|
||||
const hashes = await this.hashes(1, `${remote}/integration`);
|
||||
|
||||
return hashes[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of a certain remote.
|
||||
*
|
||||
* @param remote Remote name.
|
||||
* @return Promise resolved with the remote URL.
|
||||
*/
|
||||
getRemoteUrl(remote) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`git remote get-url ${remote}`, (err, url) => {
|
||||
if (url) {
|
||||
resolve(url.replace('\n', ''));
|
||||
} else {
|
||||
reject (err || 'Remote not found.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latest hashes from git log.
|
||||
*
|
||||
* @param count Number of commits to display.
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param format Pretty-print the contents of the commit logs in a given format.
|
||||
* @return Promise resolved with the list of hashes.
|
||||
*/
|
||||
async hashes(count, range, format) {
|
||||
format = format || '%H';
|
||||
|
||||
const hashList = await this.log(count, range, format);
|
||||
|
||||
const hashes = hashList.split('\n');
|
||||
hashes.pop(); // Remove last element, it's an empty string.
|
||||
|
||||
return hashes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the log command and returns the raw output.
|
||||
*
|
||||
* @param count Number of commits to display.
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param format Pretty-print the contents of the commit logs in a given format.
|
||||
* @param path Show only commits that are enough to explain how the files that match the specified paths came to be.
|
||||
* @return Promise resolved with the result.
|
||||
*/
|
||||
log(count, range, format, path) {
|
||||
if (typeof count == 'undefined') {
|
||||
count = 10;
|
||||
}
|
||||
|
||||
let command = 'git log';
|
||||
|
||||
if (count > 0) {
|
||||
command += ` -n ${count} `;
|
||||
}
|
||||
if (format) {
|
||||
command += ` --format=${format} `;
|
||||
}
|
||||
if (range){
|
||||
command += ` ${range} `;
|
||||
}
|
||||
if (path) {
|
||||
command += ` -- ${path}`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (err, result, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latest titles of the commit messages.
|
||||
*
|
||||
* @param count Number of commits to display.
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param path Show only commits that are enough to explain how the files that match the specified paths came to be.
|
||||
* @return Promise resolved with the list of titles.
|
||||
*/
|
||||
async messages(count, range, path) {
|
||||
count = typeof count != 'undefined' ? count : 10;
|
||||
|
||||
const messageList = await this.log(count, range, '%s', path);
|
||||
|
||||
const messages = messageList.split('\n');
|
||||
messages.pop(); // Remove last element, it's an empty string.
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a branch.
|
||||
*
|
||||
* @param remote Remote to use.
|
||||
* @param branch Branch to push.
|
||||
* @param force Whether to force the push.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
push(remote, branch, force) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let command = `git push ${remote} ${branch}`;
|
||||
if (force) {
|
||||
command += ' -f';
|
||||
}
|
||||
|
||||
exec(command, (err, result, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Git();
|
|
@ -0,0 +1,475 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const exec = require('child_process').exec;
|
||||
const https = require('https');
|
||||
const inquirer = require('inquirer');
|
||||
const fs = require('fs');
|
||||
const request = require('request'); // This lib is deprecated, but it was the only way I found to make upload files work.
|
||||
const DevConfig = require('./dev-config');
|
||||
const Git = require('./git');
|
||||
const Url = require('./url');
|
||||
const Utils = require('./utils');
|
||||
|
||||
const apiVersion = 2;
|
||||
|
||||
/**
|
||||
* Class to interact with Jira.
|
||||
*/
|
||||
class Jira {
|
||||
|
||||
/**
|
||||
* Ask the password to the user.
|
||||
*
|
||||
* @return Promise resolved with the password.
|
||||
*/
|
||||
async askPassword() {
|
||||
const data = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: `Please enter the password for the username ${this.username}.`,
|
||||
},
|
||||
]);
|
||||
|
||||
return data.password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user the tracker data.
|
||||
*
|
||||
* @return Promise resolved with the data, rejected if cannot get.
|
||||
*/
|
||||
async askTrackerData() {
|
||||
const data = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'url',
|
||||
message: 'Please enter the tracker URL.',
|
||||
default: 'https://tracker.moodle.org/',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Please enter your tracker username.',
|
||||
},
|
||||
]);
|
||||
|
||||
DevConfig.save({
|
||||
'tracker.url': data.url,
|
||||
'tracker.username': data.username,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build URL to perform requests to Jira.
|
||||
*
|
||||
* @param uri URI to add the the Jira URL.
|
||||
* @return URL.
|
||||
*/
|
||||
buildRequestUrl(uri) {
|
||||
return Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an attachment.
|
||||
*
|
||||
* @param attachmentId Attachment ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async deleteAttachment(attachmentId) {
|
||||
const response = await this.request(`attachment/${attachmentId}`, 'DELETE');
|
||||
|
||||
if (response.status != 204) {
|
||||
throw new Error('Could not delete the attachment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the issue info from jira server using a REST API call.
|
||||
*
|
||||
* @param key Key to identify the issue. E.g. MOBILE-1234.
|
||||
* @param fields Fields to get.
|
||||
* @return Promise resolved with the issue data.
|
||||
*/
|
||||
async getIssue(key, fields) {
|
||||
fields = fields || '*all,-comment';
|
||||
|
||||
await this.init(); // Initialize data if needed.
|
||||
|
||||
const response = await this.request(`issue/${key}`, 'GET', {'fields': fields, 'expand': 'names'});
|
||||
|
||||
if (response.status == 404) {
|
||||
throw new Error('Issue could not be found.');
|
||||
} else if (response.status != 200) {
|
||||
throw new Error('The tracker is not available.')
|
||||
}
|
||||
|
||||
const issue = response.data;
|
||||
issue.named = {};
|
||||
|
||||
// Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID.
|
||||
const nameList = issue.names || {};
|
||||
for (const fieldKey in issue.fields) {
|
||||
if (nameList[fieldKey]) {
|
||||
issue.named[nameList[fieldKey]] = issue.fields[fieldKey];
|
||||
}
|
||||
}
|
||||
|
||||
return issue
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the version info from the jira server using a rest api call.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async getServerInfo() {
|
||||
const response = await this.request('serverInfo');
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(`Unexpected response code: ${response.status}`, response);
|
||||
}
|
||||
|
||||
this.version = response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracker data to push an issue.
|
||||
*
|
||||
* @return Promise resolved with the data.
|
||||
*/
|
||||
async getTrackerData() {
|
||||
// Check dev-config file first.
|
||||
let data = this.getTrackerDataFromDevConfig();
|
||||
|
||||
if (data) {
|
||||
console.log('Using tracker data from dev-config file');
|
||||
return data;
|
||||
}
|
||||
|
||||
// Try to use mdk now.
|
||||
try {
|
||||
data = await this.getTrackerDataFromMdk();
|
||||
|
||||
console.log('Using tracker data from mdk');
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
// MDK not available or not configured. Ask for the data.
|
||||
const data = await this.askTrackerData();
|
||||
|
||||
data.fromInput = true;
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracker data from dev config file.
|
||||
*
|
||||
* @return Data, undefined if cannot get.
|
||||
*/
|
||||
getTrackerDataFromDevConfig() {
|
||||
const url = DevConfig.get('tracker.url');
|
||||
const username = DevConfig.get('tracker.username');
|
||||
|
||||
if (url && username) {
|
||||
return {
|
||||
url,
|
||||
username,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracker URL and username from mdk.
|
||||
*
|
||||
* @return Promise resolved with the data, rejected if cannot get.
|
||||
*/
|
||||
getTrackerDataFromMdk() {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('mdk config show tracker.url', (err, url) => {
|
||||
if (!url) {
|
||||
reject(err || 'URL not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
exec('mdk config show tracker.username', (err, username) => {
|
||||
if (username) {
|
||||
resolve({
|
||||
url: url.replace('\n', ''),
|
||||
username: username.replace('\n', ''),
|
||||
});
|
||||
} else {
|
||||
reject(err | 'Username not found.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize some data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async init() {
|
||||
if (this.initialized) {
|
||||
// Already initialized.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tracker URL and username.
|
||||
const trackerData = await this.getTrackerData();
|
||||
|
||||
this.url = trackerData.url;
|
||||
this.username = trackerData.username;
|
||||
|
||||
const parsed = Url.parse(this.url);
|
||||
this.ssl = parsed.protocol == 'https';
|
||||
this.host = parsed.domain;
|
||||
this.uri = parsed.path;
|
||||
|
||||
// Get the password.
|
||||
const keytar = require('keytar');
|
||||
|
||||
this.password = await keytar.getPassword('mdk-jira-password', this.username); // Use same service name as mdk.
|
||||
|
||||
if (!this.password) {
|
||||
// Ask the user.
|
||||
this.password = await this.askPassword();
|
||||
}
|
||||
|
||||
while (!this.initialized) {
|
||||
try {
|
||||
await this.getServerInfo();
|
||||
|
||||
this.initialized = true;
|
||||
keytar.setPassword('mdk-jira-password', this.username, this.password);
|
||||
} catch (error) {
|
||||
console.log('Error connecting to the server. Please make sure you entered the data correctly.', error);
|
||||
if (trackerData.fromInput) {
|
||||
// User entered the data manually, ask him again.
|
||||
trackerData = await this.askTrackerData();
|
||||
|
||||
this.url = trackerData.url;
|
||||
this.username = trackerData.username;
|
||||
}
|
||||
|
||||
this.password = await this.askPassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a certain issue could be a security issue.
|
||||
*
|
||||
* @param key Key to identify the issue. E.g. MOBILE-1234.
|
||||
* @return Promise resolved with boolean: whether it's a security issue.
|
||||
*/
|
||||
async isSecurityIssue(key) {
|
||||
const issue = await this.getIssue(key, 'security');
|
||||
|
||||
return issue.fields && !!issue.fields.security;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the server and returns the data.
|
||||
*
|
||||
* @param uri URI to add the the Jira URL.
|
||||
* @param method Method to use. Defaults to 'GET'.
|
||||
* @param params Params to send as GET params (in the URL).
|
||||
* @param data JSON string with the data to send as POST/PUT params.
|
||||
* @param headers Headers to send.
|
||||
* @return Promise resolved with the result.
|
||||
*/
|
||||
request(uri, method, params, data, headers) {
|
||||
uri = uri || '';
|
||||
method = (method || 'GET').toUpperCase();
|
||||
data = data || '';
|
||||
params = params || {};
|
||||
headers = headers || {};
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Build the request URL.
|
||||
const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params);
|
||||
|
||||
// Initialize the request.
|
||||
const options = {
|
||||
method: method,
|
||||
auth: `${this.username}:${this.password}`,
|
||||
headers: headers,
|
||||
};
|
||||
const request = https.request(url, options);
|
||||
|
||||
// Add data.
|
||||
if (data) {
|
||||
request.write(data);
|
||||
}
|
||||
|
||||
// Treat response.
|
||||
request.on('response', (response) => {
|
||||
// Read the result.
|
||||
let result = '';
|
||||
response.on('data', (chunk) => {
|
||||
result += chunk;
|
||||
});
|
||||
response.on('end', () => {
|
||||
try {
|
||||
result = JSON.parse(result);
|
||||
} catch (error) {
|
||||
// Leave it as text.
|
||||
}
|
||||
|
||||
resolve({
|
||||
status: response.statusCode,
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (e) => {
|
||||
reject(e);
|
||||
});
|
||||
|
||||
// Send the request.
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a set of fields for a certain issue in Jira.
|
||||
*
|
||||
* @param key Key to identify the issue. E.g. MOBILE-1234.
|
||||
* @param updates Object with the fields to update.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async setCustomFields(key, updates) {
|
||||
const issue = await this.getIssue(key);
|
||||
const update = {'fields': {}};
|
||||
|
||||
// Detect which fields have changed.
|
||||
for (const updateName in updates) {
|
||||
const updateValue = updates[updateName];
|
||||
const remoteValue = issue.named[updateName];
|
||||
|
||||
if (!remoteValue || remoteValue != updateValue) {
|
||||
// Map the label of the field with the field code.
|
||||
let fieldKey;
|
||||
for (const key in issue.names) {
|
||||
if (issue.names[key] == updateName) {
|
||||
fieldKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fieldKey) {
|
||||
throw new Error(`Could not find the field named ${updateName}.`);
|
||||
}
|
||||
|
||||
update.fields[fieldKey] = updateValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(update.fields).length) {
|
||||
// No fields to update.
|
||||
console.log('No updates required.')
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.request(`issue/${key}`, 'PUT', null, JSON.stringify(update));
|
||||
|
||||
if (response.status != 204) {
|
||||
throw new Error(`Issue was not updated: ${response.status}`, response.data);
|
||||
}
|
||||
|
||||
console.log('Issue updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a new attachment to an issue.
|
||||
*
|
||||
* @param key Key to identify the issue. E.g. MOBILE-1234.
|
||||
* @param filePath Path to the file to upload.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async upload(key, filePath) {
|
||||
|
||||
const uri = `issue/${key}/attachments`;
|
||||
const headers = {
|
||||
'X-Atlassian-Token': 'nocheck',
|
||||
}
|
||||
|
||||
const response = await this.uploadFile(uri, 'file', filePath, headers);
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error('Could not upload file to Jira issue');
|
||||
}
|
||||
|
||||
console.log('File successfully uploaded.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Jira.
|
||||
*
|
||||
* @param uri URI to add the the Jira URL.
|
||||
* @param fieldName Name of the form field where to put the file.
|
||||
* @param filePath Path to the file.
|
||||
* @param headers Headers.
|
||||
* @return Promise resolved with the result.
|
||||
*/
|
||||
async uploadFile(uri, fieldName, filePath, headers) {
|
||||
uri = uri || '';
|
||||
headers = headers || {};
|
||||
headers['Content-Type'] = 'multipart/form-data';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Add the file to the form data.
|
||||
const formData = {};
|
||||
formData[fieldName] = {
|
||||
value: fs.createReadStream(filePath),
|
||||
options: {
|
||||
filename: filePath.substr(filePath.lastIndexOf('/') + 1),
|
||||
contentType: 'multipart/form-data',
|
||||
},
|
||||
};
|
||||
|
||||
// Perform the request.
|
||||
const options = {
|
||||
url: this.buildRequestUrl(uri),
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
auth: {
|
||||
user: this.username,
|
||||
pass: this.password,
|
||||
},
|
||||
formData: formData,
|
||||
};
|
||||
|
||||
request(options, (err, httpResponse, body) => {
|
||||
resolve({
|
||||
status: httpResponse.statusCode,
|
||||
data: body,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Jira();
|
|
@ -0,0 +1,138 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const gulp = require('gulp');
|
||||
const through = require('through');
|
||||
const bufferFrom = require('buffer-from');
|
||||
const rename = require('gulp-rename');
|
||||
const exec = require('child_process').exec;
|
||||
|
||||
const LICENSE = '' +
|
||||
'// (C) Copyright 2015 Moodle Pty Ltd.\n' +
|
||||
'//\n' +
|
||||
'// Licensed under the Apache License, Version 2.0 (the "License");\n' +
|
||||
'// you may not use this file except in compliance with the License.\n' +
|
||||
'// You may obtain a copy of the License at\n' +
|
||||
'//\n' +
|
||||
'// http://www.apache.org/licenses/LICENSE-2.0\n' +
|
||||
'//\n' +
|
||||
'// Unless required by applicable law or agreed to in writing, software\n' +
|
||||
'// distributed under the License is distributed on an "AS IS" BASIS,\n' +
|
||||
'// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' +
|
||||
'// See the License for the specific language governing permissions and\n' +
|
||||
'// limitations under the License.\n\n';
|
||||
|
||||
/**
|
||||
* Task to convert config.json into a TypeScript class.
|
||||
*/
|
||||
class BuildConfigTask {
|
||||
|
||||
/**
|
||||
* Run the task.
|
||||
*
|
||||
* @param path Path to the config file.
|
||||
* @param done Function to call when done.
|
||||
*/
|
||||
run(path, done) {
|
||||
const self = this;
|
||||
|
||||
// Get the last commit.
|
||||
exec('git log -1 --pretty=format:"%H"', (err, commit, stderr) => {
|
||||
if (err) {
|
||||
console.error('An error occurred while getting the last commit: ' + err);
|
||||
} else if (stderr) {
|
||||
console.error('An error occurred while getting the last commit: ' + stderr);
|
||||
}
|
||||
|
||||
gulp.src(path)
|
||||
.pipe(through(function(file) {
|
||||
// Convert the contents of the file into a TypeScript class.
|
||||
// Disable the rule variable-name in the file.
|
||||
const config = JSON.parse(file.contents.toString());
|
||||
let contents = LICENSE + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n';
|
||||
|
||||
for (let key in config) {
|
||||
let value = self.transformValue(config[key]);
|
||||
|
||||
if (typeof config[key] != 'number' && typeof config[key] != 'boolean' && typeof config[key] != 'string') {
|
||||
key = key + ': any';
|
||||
}
|
||||
|
||||
// If key has quotation marks, remove them.
|
||||
if (key[0] == '"') {
|
||||
key = key.substr(1, key.length - 2);
|
||||
}
|
||||
contents += ' static ' + key + ' = ' + value + ';\n';
|
||||
}
|
||||
|
||||
// Add compilation info.
|
||||
contents += ' static compilationtime = ' + Date.now() + ';\n';
|
||||
contents += ' static lastcommit = \'' + commit + '\';\n';
|
||||
|
||||
contents += '}\n';
|
||||
|
||||
file.contents = bufferFrom(contents);
|
||||
|
||||
this.emit('data', file);
|
||||
}))
|
||||
.pipe(rename('configconstants.ts'))
|
||||
.pipe(gulp.dest('./src'))
|
||||
.on('end', done);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively transform a config value into personalized TS.
|
||||
*
|
||||
* @param value Value to convert
|
||||
* @return Converted value.
|
||||
*/
|
||||
transformValue(value) {
|
||||
if (typeof value == 'string') {
|
||||
// Wrap the string in ' and escape them.
|
||||
return "'" + value.replace(/([^\\])'/g, "$1\\'") + "'";
|
||||
}
|
||||
|
||||
if (typeof value != 'number' && typeof value != 'boolean') {
|
||||
const isArray = Array.isArray(value);
|
||||
let contents = '';
|
||||
|
||||
let quoteKeys = false;
|
||||
if (!isArray) {
|
||||
for (let key in value) {
|
||||
if (key.indexOf('-') >= 0) {
|
||||
quoteKeys = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let key in value) {
|
||||
value[key] = this.transformValue(value[key]);
|
||||
|
||||
const quotedKey = quoteKeys ? "'" + key + "'" : key;
|
||||
contents += ' ' + (isArray ? '' : quotedKey + ': ') + value[key] + ",\n";
|
||||
}
|
||||
|
||||
contents += (isArray ? ']' : '}');
|
||||
|
||||
return (isArray ? '[' : '{') + "\n" + contents.replace(/^/gm, ' ');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BuildConfigTask;
|
|
@ -0,0 +1,176 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const gulp = require('gulp');
|
||||
const slash = require('gulp-slash');
|
||||
const clipEmptyFiles = require('gulp-clip-empty-files');
|
||||
const through = require('through');
|
||||
const bufferFrom = require('buffer-from');
|
||||
const File = require('vinyl');
|
||||
const pathLib = require('path');
|
||||
|
||||
/**
|
||||
* Task to build the language files into a single file per language.
|
||||
*/
|
||||
class BuildLangTask {
|
||||
|
||||
/**
|
||||
* Copy a property from one object to another, adding a prefix to the key if needed.
|
||||
*
|
||||
* @param target Object to copy the properties to.
|
||||
* @param source Object to copy the properties from.
|
||||
* @param prefix Prefix to add to the keys.
|
||||
*/
|
||||
addProperties(target, source, prefix) {
|
||||
for (let property in source) {
|
||||
target[prefix + property] = source[property];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task.
|
||||
*
|
||||
* @param language Language to treat.
|
||||
* @param langPaths Paths to the possible language files.
|
||||
* @param done Function to call when done.
|
||||
*/
|
||||
run(language, langPaths, done) {
|
||||
const filename = language + '.json';
|
||||
const data = {};
|
||||
let firstFile = null;
|
||||
const self = this;
|
||||
|
||||
const paths = langPaths.map((path) => {
|
||||
if (path.slice(-1) != '/') {
|
||||
path = path + '/';
|
||||
}
|
||||
|
||||
return path + language + '.json';
|
||||
});
|
||||
|
||||
gulp.src(paths, { allowEmpty: true })
|
||||
.pipe(slash())
|
||||
.pipe(clipEmptyFiles())
|
||||
.pipe(through(function(file) {
|
||||
if (!firstFile) {
|
||||
firstFile = file;
|
||||
}
|
||||
|
||||
return self.treatFile(file, data);
|
||||
}, function() {
|
||||
/* This implementation is based on gulp-jsoncombine module.
|
||||
* https://github.com/reflog/gulp-jsoncombine */
|
||||
if (firstFile) {
|
||||
const joinedPath = pathLib.join(firstFile.base, language + '.json');
|
||||
|
||||
const joinedFile = new File({
|
||||
cwd: firstFile.cwd,
|
||||
base: firstFile.base,
|
||||
path: joinedPath,
|
||||
contents: self.treatMergedData(data),
|
||||
});
|
||||
|
||||
this.emit('data', joinedFile);
|
||||
}
|
||||
|
||||
this.emit('end');
|
||||
}))
|
||||
.pipe(gulp.dest(pathLib.join('./src/assets', 'lang')))
|
||||
.on('end', done);
|
||||
}
|
||||
|
||||
/**
|
||||
* Treats a file to merge JSONs. This function is based on gulp-jsoncombine module.
|
||||
* https://github.com/reflog/gulp-jsoncombine
|
||||
*
|
||||
* @param file File treated.
|
||||
* @param data Object where to store the data.
|
||||
*/
|
||||
treatFile(file, data) {
|
||||
if (file.isNull() || file.isStream()) {
|
||||
return; // ignore
|
||||
}
|
||||
|
||||
try {
|
||||
let srcPos = file.path.lastIndexOf('/src/');
|
||||
if (srcPos == -1) {
|
||||
// It's probably a Windows environment.
|
||||
srcPos = file.path.lastIndexOf('\\src\\');
|
||||
}
|
||||
|
||||
const path = file.path.substr(srcPos + 5);
|
||||
data[path] = JSON.parse(file.contents.toString());
|
||||
} catch (err) {
|
||||
console.log('Error parsing JSON: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Treats the merged JSON data, adding prefixes depending on the component.
|
||||
*
|
||||
* @param data Merged data.
|
||||
* @return Buffer with the treated data.
|
||||
*/
|
||||
treatMergedData(data) {
|
||||
const merged = {};
|
||||
const mergedOrdered = {};
|
||||
|
||||
for (let filepath in data) {
|
||||
const pathSplit = filepath.split(/[\/\\]/);
|
||||
let prefix;
|
||||
|
||||
pathSplit.pop();
|
||||
|
||||
switch (pathSplit[0]) {
|
||||
case 'lang':
|
||||
prefix = 'core';
|
||||
break;
|
||||
case 'core':
|
||||
if (pathSplit[1] == 'lang') {
|
||||
// Not used right now.
|
||||
prefix = 'core';
|
||||
} else {
|
||||
prefix = 'core.' + pathSplit[1];
|
||||
}
|
||||
break;
|
||||
case 'addon':
|
||||
// Remove final item 'lang'.
|
||||
pathSplit.pop();
|
||||
// Remove first item 'addon'.
|
||||
pathSplit.shift();
|
||||
|
||||
// For subplugins. We'll use plugin_subfolder_subfolder2_...
|
||||
// E.g. 'mod_assign_feedback_comments'.
|
||||
prefix = 'addon.' + pathSplit.join('_');
|
||||
break;
|
||||
case 'assets':
|
||||
prefix = 'assets.' + pathSplit[1];
|
||||
break;
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
this.addProperties(merged, data[filepath], prefix + '.');
|
||||
}
|
||||
}
|
||||
|
||||
// Force ordering by string key.
|
||||
Object.keys(merged).sort().forEach((key) => {
|
||||
mergedOrdered[key] = merged[key];
|
||||
});
|
||||
|
||||
return bufferFrom(JSON.stringify(mergedOrdered, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BuildLangTask;
|
|
@ -0,0 +1,164 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const gulp = require('gulp');
|
||||
const through = require('through');
|
||||
const bufferFrom = require('buffer-from');
|
||||
const concat = require('gulp-concat');
|
||||
const pathLib = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Task to combine scss into a single file.
|
||||
*/
|
||||
class CombineScssTask {
|
||||
|
||||
/**
|
||||
* Finds the file and returns its content.
|
||||
*
|
||||
* @param capture Import file path.
|
||||
* @param baseDir Directory where the file was found.
|
||||
* @param paths Alternative paths where to find the imports.
|
||||
* @param parsedFiles Already parsed files to reduce size of the result.
|
||||
* @return Partially combined scss.
|
||||
*/
|
||||
getReplace(capture, baseDir, paths, parsedFiles) {
|
||||
let parse = pathLib.parse(pathLib.resolve(baseDir, capture + '.scss'));
|
||||
let file = parse.dir + '/' + parse.name;
|
||||
|
||||
if (file.slice(-3) === '.wp') {
|
||||
console.log('Windows Phone not supported "' + capture);
|
||||
// File was already parsed, leave the import commented.
|
||||
return '// @import "' + capture + '";';
|
||||
}
|
||||
|
||||
if (!fs.existsSync(file + '.scss')) {
|
||||
// File not found, might be a partial file.
|
||||
file = parse.dir + '/_' + parse.name;
|
||||
}
|
||||
|
||||
// If file still not found, try to find the file in the alternative paths.
|
||||
let x = 0;
|
||||
while (!fs.existsSync(file + '.scss') && paths.length > x) {
|
||||
parse = pathLib.parse(pathLib.resolve(paths[x], capture + '.scss'));
|
||||
file = parse.dir + '/' + parse.name;
|
||||
|
||||
x++;
|
||||
}
|
||||
|
||||
file = file + '.scss';
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
// File not found. Leave the import there.
|
||||
console.log('File "' + capture + '" not found');
|
||||
return '@import "' + capture + '";';
|
||||
}
|
||||
|
||||
if (parsedFiles.indexOf(file) >= 0) {
|
||||
console.log('File "' + capture + '" already parsed');
|
||||
// File was already parsed, leave the import commented.
|
||||
return '// @import "' + capture + '";';
|
||||
}
|
||||
|
||||
parsedFiles.push(file);
|
||||
const text = fs.readFileSync(file);
|
||||
|
||||
// Recursive call.
|
||||
return this.scssCombine(text, parse.dir, paths, parsedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task.
|
||||
*
|
||||
* @param done Function to call when done.
|
||||
*/
|
||||
run(done) {
|
||||
const paths = [
|
||||
'node_modules/ionic-angular/themes/',
|
||||
'node_modules/font-awesome/scss/',
|
||||
'node_modules/ionicons/dist/scss/'
|
||||
];
|
||||
const parsedFiles = [];
|
||||
const self = this;
|
||||
|
||||
gulp.src([
|
||||
'./src/theme/variables.scss',
|
||||
'./node_modules/ionic-angular/themes/ionic.globals.*.scss',
|
||||
'./node_modules/ionic-angular/themes/ionic.components.scss',
|
||||
'./src/**/*.scss',
|
||||
]).pipe(through(function(file) { // Combine them based on @import and save it to stream.
|
||||
if (file.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
parsedFiles.push(file);
|
||||
file.contents = bufferFrom(self.scssCombine(
|
||||
file.contents, pathLib.dirname(file.path), paths, parsedFiles));
|
||||
|
||||
this.emit('data', file);
|
||||
})).pipe(concat('combined.scss')) // Concat the stream output in single file.
|
||||
.pipe(gulp.dest('.')) // Save file to destination.
|
||||
.on('end', done);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine scss files with its imports
|
||||
*
|
||||
* @param content Scss string to treat.
|
||||
* @param baseDir Directory where the file was found.
|
||||
* @param paths Alternative paths where to find the imports.
|
||||
* @param parsedFiles Already parsed files to reduce size of the result.
|
||||
* @return Scss string with the replaces done.
|
||||
*/
|
||||
scssCombine(content, baseDir, paths, parsedFiles) {
|
||||
// Content is a Buffer, convert to string.
|
||||
if (typeof content != "string") {
|
||||
content = content.toString();
|
||||
}
|
||||
|
||||
// Search of single imports.
|
||||
let regex = /@import[ ]*['"](.*)['"][ ]*;/g;
|
||||
|
||||
if (regex.test(content)) {
|
||||
return content.replace(regex, (m, capture) => {
|
||||
if (capture == "bmma") {
|
||||
return m;
|
||||
}
|
||||
|
||||
return this.getReplace(capture, baseDir, paths, parsedFiles);
|
||||
});
|
||||
}
|
||||
|
||||
// Search of multiple imports.
|
||||
regex = /@import(?:[ \n]+['"](.*)['"][,]?[ \n]*)+;/gm;
|
||||
if (regex.test(content)) {
|
||||
return content.replace(regex, (m, capture) => {
|
||||
let text = '';
|
||||
|
||||
// Divide the import into multiple files.
|
||||
const captures = m.match(/['"]([^'"]*)['"]/g);
|
||||
|
||||
for (let x in captures) {
|
||||
text += this.getReplace(captures[x].replace(/['"]+/g, ''), baseDir, paths, parsedFiles) + '\n';
|
||||
}
|
||||
|
||||
return text;
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CombineScssTask;
|
|
@ -0,0 +1,79 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const fs = require('fs');
|
||||
const gulp = require('gulp');
|
||||
const flatten = require('gulp-flatten');
|
||||
const htmlmin = require('gulp-htmlmin');
|
||||
const pathLib = require('path');
|
||||
|
||||
const TEMPLATES_SRC = [
|
||||
'./src/components/**/*.html',
|
||||
'./src/core/**/components/**/*.html',
|
||||
'./src/core/**/component/**/*.html',
|
||||
// Copy all addon components because any component can be injected using extraImports.
|
||||
'./src/addon/**/components/**/*.html',
|
||||
'./src/addon/**/component/**/*.html'
|
||||
];
|
||||
const TEMPLATES_DEST = './www/templates';
|
||||
|
||||
/**
|
||||
* Task to copy component templates to www to make compile-html work in AOT.
|
||||
*/
|
||||
class CopyComponentTemplatesTask {
|
||||
|
||||
/**
|
||||
* Delete a folder and all its contents.
|
||||
*
|
||||
* @param path [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
deleteFolderRecursive(path) {
|
||||
if (fs.existsSync(path)) {
|
||||
fs.readdirSync(path).forEach((file) => {
|
||||
var curPath = pathLib.join(path, file);
|
||||
|
||||
if (fs.lstatSync(curPath).isDirectory()) {
|
||||
this.deleteFolderRecursive(curPath);
|
||||
} else {
|
||||
fs.unlinkSync(curPath);
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmdirSync(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task.
|
||||
*
|
||||
* @param done Callback to call once done.
|
||||
*/
|
||||
run(done) {
|
||||
this.deleteFolderRecursive(TEMPLATES_DEST);
|
||||
|
||||
gulp.src(TEMPLATES_SRC, { allowEmpty: true })
|
||||
.pipe(flatten())
|
||||
// Check options here: https://github.com/kangax/html-minifier
|
||||
.pipe(htmlmin({
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
caseSensitive: true
|
||||
}))
|
||||
.pipe(gulp.dest(TEMPLATES_DEST))
|
||||
.on('end', done);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CopyComponentTemplatesTask;
|
|
@ -0,0 +1,280 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const gulp = require('gulp');
|
||||
const inquirer = require('inquirer');
|
||||
const DevConfig = require('./dev-config');
|
||||
const Git = require('./git');
|
||||
const Jira = require('./jira');
|
||||
const Utils = require('./utils');
|
||||
|
||||
/**
|
||||
* Task to push a git branch and update tracker issue.
|
||||
*/
|
||||
class PushTask {
|
||||
|
||||
/**
|
||||
* Ask the user whether he wants to continue.
|
||||
*
|
||||
* @return Promise resolved with boolean: true if he wants to continue.
|
||||
*/
|
||||
async askConfirmContinue() {
|
||||
const answer = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'confirm',
|
||||
message: 'Are you sure you want to continue?',
|
||||
default: 'n',
|
||||
},
|
||||
]);
|
||||
|
||||
return answer.confirm == 'y';
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a patch to the tracker and remove the previous one.
|
||||
*
|
||||
* @param branch Branch name.
|
||||
* @param branchData Parsed branch data.
|
||||
* @param remote Remote used.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async pushPatch(branch, branchData, remote) {
|
||||
const headCommit = await Git.getHeadCommit(branch, branchData);
|
||||
|
||||
if (!headCommit) {
|
||||
throw new Error('Head commit not resolved, abort pushing patch.');
|
||||
}
|
||||
|
||||
// Create the patch file.
|
||||
const fileName = branch + '.patch';
|
||||
const tmpPatchPath = `./tmp/${fileName}`;
|
||||
|
||||
await Git.createPatch(`${headCommit}...${branch}`, tmpPatchPath);
|
||||
console.log('Git patch created');
|
||||
|
||||
// Check if there is an attachment with same name in the issue.
|
||||
const issue = await Jira.getIssue(branchData.issue, 'attachment');
|
||||
|
||||
let existingAttachmentId;
|
||||
const attachments = (issue.fields && issue.fields.attachment) || [];
|
||||
for (const i in attachments) {
|
||||
if (attachments[i].filename == fileName) {
|
||||
// Found an existing attachment with the same name, we keep track of it.
|
||||
existingAttachmentId = attachments[i].id;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Push the patch to the tracker.
|
||||
console.log(`Uploading patch ${fileName} to the tracker...`);
|
||||
await Jira.upload(branchData.issue, tmpPatchPath);
|
||||
|
||||
if (existingAttachmentId) {
|
||||
// On success, deleting file that was there before.
|
||||
try {
|
||||
console.log('Deleting older patch...')
|
||||
await Jira.deleteAttachment(existingAttachmentId);
|
||||
} catch (error) {
|
||||
console.log('Could not delete older attachment.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task.
|
||||
*
|
||||
* @param args Command line arguments.
|
||||
* @param done Function to call when done.
|
||||
*/
|
||||
async run(args, done) {
|
||||
try {
|
||||
const remote = args.remote || DevConfig.get('upstreamRemote', 'origin');
|
||||
let branch = args.branch;
|
||||
const force = !!args.force;
|
||||
|
||||
if (!branch) {
|
||||
branch = await Git.getCurrentBranch();
|
||||
}
|
||||
|
||||
if (!branch) {
|
||||
throw new Error('Cannot determine the current branch. Please make sure youu aren\'t in detached HEAD state');
|
||||
} else if (branch == 'HEAD') {
|
||||
throw new Error('Cannot push HEAD branch');
|
||||
}
|
||||
|
||||
// Parse the branch to get the project and issue number.
|
||||
const branchData = Utils.parseBranch(branch);
|
||||
const keepRunning = await this.validateCommitMessages(branchData);
|
||||
|
||||
if (!keepRunning) {
|
||||
// Last commit not valid, stop.
|
||||
console.log('Exiting...');
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.patch) {
|
||||
// Check if it's a security issue to force patch mode.
|
||||
try {
|
||||
args.patch = await Jira.isSecurityIssue(branchData.issue);
|
||||
|
||||
if (args.patch) {
|
||||
console.log(`${branchData.issue} appears to be a security issue, switching to patch mode...`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Could not check if ${branchData.issue} is a security issue.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.patch) {
|
||||
// Create and upload a patch file.
|
||||
await this.pushPatch(branch, branchData, remote);
|
||||
} else {
|
||||
// Push the branch.
|
||||
console.log(`Pushing branch ${branch} to remote ${remote}...`);
|
||||
await Git.push(remote, branch, force);
|
||||
|
||||
// Update tracker info.
|
||||
console.log(`Branch pushed, update tracker info...`);
|
||||
await this.updateTrackerGitInfo(branch, branchData, remote);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update git info in the tracker issue.
|
||||
*
|
||||
* @param branch Branch name.
|
||||
* @param branchData Parsed branch data.
|
||||
* @param remote Remote used.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async updateTrackerGitInfo(branch, branchData, remote) {
|
||||
// Get the repository data for the project.
|
||||
let repositoryUrl = DevConfig.get(branchData.project + '.repositoryUrl');
|
||||
let diffUrlTemplate = DevConfig.get(branchData.project + '.diffUrlTemplate', '');
|
||||
|
||||
if (!repositoryUrl) {
|
||||
// Calculate the repositoryUrl based on the remote URL.
|
||||
repositoryUrl = await Git.getRemoteUrl(remote);
|
||||
}
|
||||
|
||||
// Make sure the repository URL uses the regular format.
|
||||
repositoryUrl = repositoryUrl.replace(/^(git@|git:\/\/)/, 'https://')
|
||||
.replace(/\.git$/, '')
|
||||
.replace('github.com:', 'github.com/');
|
||||
|
||||
if (!diffUrlTemplate) {
|
||||
diffUrlTemplate = Utils.concatenatePaths([repositoryUrl, 'compare/%headcommit%...%branch%']);
|
||||
}
|
||||
|
||||
// Now create the git URL for the repository.
|
||||
const repositoryGitUrl = repositoryUrl.replace(/^https?:\/\//, 'git://') + '.git';
|
||||
|
||||
// Search HEAD commit to put in the diff URL.
|
||||
console.log ('Searching for head commit...');
|
||||
let headCommit = await Git.getHeadCommit(branch, branchData);
|
||||
|
||||
if (!headCommit) {
|
||||
throw new Error('Head commit not resolved, aborting update of tracker fields');
|
||||
}
|
||||
|
||||
headCommit = headCommit.substr(0, 10);
|
||||
console.log(`Head commit resolved to ${headCommit}`);
|
||||
|
||||
// Calculate last properties needed.
|
||||
const diffUrl = diffUrlTemplate.replace('%branch%', branch).replace('%headcommit%', headCommit);
|
||||
const fieldRepositoryUrl = DevConfig.get('tracker.fieldnames.repositoryurl', 'Pull from Repository');
|
||||
const fieldBranch = DevConfig.get('tracker.fieldnames.branch', 'Pull Master Branch');
|
||||
const fieldDiffUrl = DevConfig.get('tracker.fieldnames.diffurl', 'Pull Master Diff URL');
|
||||
|
||||
// Update tracker fields.
|
||||
const updates = {};
|
||||
updates[fieldRepositoryUrl] = repositoryGitUrl;
|
||||
updates[fieldBranch] = branch;
|
||||
updates[fieldDiffUrl] = diffUrl;
|
||||
|
||||
console.log('Setting tracker fields...');
|
||||
await Jira.setCustomFields(branchData.issue, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate commit messages comparing them with the branch name.
|
||||
*
|
||||
* @param branchData Parsed branch data.
|
||||
* @return True if value is ok or the user wants to continue anyway, false to stop.
|
||||
*/
|
||||
async validateCommitMessages(branchData) {
|
||||
const messages = await Git.messages(30);
|
||||
|
||||
let numConsecutive = 0;
|
||||
let wrongCommitCandidate = null;
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
const issue = Utils.getIssueFromCommitMessage(message);
|
||||
|
||||
if (!issue || issue != branchData.issue) {
|
||||
if (i === 0) {
|
||||
// Last commit is wrong, it shouldn't happen. Ask the user if he wants to continue.
|
||||
if (!issue) {
|
||||
console.log('The issue number could not be found in the last commit message.');
|
||||
console.log(`Commit: ${message}`);
|
||||
} else if (issue != branchData.issue) {
|
||||
console.log('The issue number in the last commit does not match the branch being pushed to.');
|
||||
console.log(`Branch: ${branchData.issue} vs. commit: ${issue}`);
|
||||
}
|
||||
|
||||
return this.askConfirmContinue();
|
||||
}
|
||||
|
||||
numConsecutive++;
|
||||
if (numConsecutive > 2) {
|
||||
// 3 consecutive commits with different branch, probably the branch commits are over. Everything OK.
|
||||
return true;
|
||||
|
||||
// Don't treat a merge pull request commit as a wrong commit between right commits.
|
||||
// The current push could be a quick fix after a merge.
|
||||
} else if (!wrongCommitCandidate && message.indexOf('Merge pull request') == -1) {
|
||||
wrongCommitCandidate = {
|
||||
message: message,
|
||||
issue: issue,
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
} else if (wrongCommitCandidate) {
|
||||
// We've found a commit with the branch name after a commit with a different branch. Probably wrong commit.
|
||||
if (!wrongCommitCandidate.issue) {
|
||||
console.log('The issue number could not be found in one of the commit messages.');
|
||||
console.log(`Commit: ${wrongCommitCandidate.message}`);
|
||||
} else {
|
||||
console.log('The issue number in a certain commit does not match the branch being pushed to.');
|
||||
console.log(`Branch: ${branchData.issue} vs. commit: ${wrongCommitCandidate.issue}`);
|
||||
console.log(`Commit message: ${wrongCommitCandidate.message}`);
|
||||
}
|
||||
|
||||
return this.askConfirmContinue();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PushTask;
|
|
@ -0,0 +1,79 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Class with helper functions for urls.
|
||||
*/
|
||||
class Url {
|
||||
|
||||
/**
|
||||
* Add params to a URL.
|
||||
*
|
||||
* @param url URL to add the params to.
|
||||
* @param params Object with the params to add.
|
||||
* @return URL with params.
|
||||
*/
|
||||
static addParamsToUrl(url, params) {
|
||||
let separator = url.indexOf('?') != -1 ? '&' : '?';
|
||||
|
||||
for (const key in params) {
|
||||
let value = params[key];
|
||||
|
||||
// Ignore objects.
|
||||
if (typeof value != 'object') {
|
||||
url += separator + key + '=' + value;
|
||||
separator = '&';
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse parts of a url, using an implicit protocol if it is missing from the url.
|
||||
*
|
||||
* @param url Url.
|
||||
* @return Url parts.
|
||||
*/
|
||||
static parse(url) {
|
||||
// Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B.
|
||||
const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = match[4] || '';
|
||||
|
||||
// Get the credentials and the port from the host.
|
||||
const [domainAndPort, credentials] = host.split('@').reverse();
|
||||
const [domain, port] = domainAndPort.split(':');
|
||||
const [username, password] = credentials ? credentials.split(':') : [];
|
||||
|
||||
// Prepare parts replacing empty strings with undefined.
|
||||
return {
|
||||
protocol: match[2] || undefined,
|
||||
domain: domain || undefined,
|
||||
port: port || undefined,
|
||||
credentials: credentials || undefined,
|
||||
username: username || undefined,
|
||||
password: password || undefined,
|
||||
path: match[5] || undefined,
|
||||
query: match[7] || undefined,
|
||||
fragment: match[9] || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Url;
|
|
@ -0,0 +1,119 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
const DevConfig = require('./dev-config');
|
||||
const DEFAULT_ISSUE_REGEX = '^(MOBILE)[-_]([0-9]+)';
|
||||
|
||||
/**
|
||||
* Class with some utility functions.
|
||||
*/
|
||||
class Utils {
|
||||
/**
|
||||
* Concatenate several paths, adding a slash between them if needed.
|
||||
*
|
||||
* @param paths List of paths.
|
||||
* @return Concatenated path.
|
||||
*/
|
||||
static concatenatePaths(paths) {
|
||||
if (!paths.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove all slashes between paths.
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
if (!paths[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
paths[i] = String(paths[i]).replace(/\/+$/g, '');
|
||||
} else if (i === paths.length - 1) {
|
||||
paths[i] = String(paths[i]).replace(/^\/+/g, '');
|
||||
} else {
|
||||
paths[i] = String(paths[i]).replace(/^\/+|\/+$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty paths.
|
||||
paths = paths.filter(path => !!path);
|
||||
|
||||
return paths.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command line arguments.
|
||||
*
|
||||
* @return Object with command line arguments.
|
||||
*/
|
||||
static getCommandLineArguments() {
|
||||
|
||||
let args = {}, opt, thisOpt, curOpt;
|
||||
for (let a = 0; a < process.argv.length; a++) {
|
||||
|
||||
thisOpt = process.argv[a].trim();
|
||||
opt = thisOpt.replace(/^\-+/, '');
|
||||
|
||||
if (opt === thisOpt) {
|
||||
// argument value
|
||||
if (curOpt) {
|
||||
args[curOpt] = opt;
|
||||
}
|
||||
curOpt = null;
|
||||
}
|
||||
else {
|
||||
// Argument name.
|
||||
curOpt = opt;
|
||||
args[curOpt] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a commit message, return the issue name (e.g. MOBILE-1234).
|
||||
*
|
||||
* @param commit Commit message.
|
||||
* @return Issue name.
|
||||
*/
|
||||
static getIssueFromCommitMessage(commit) {
|
||||
const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i');
|
||||
const matches = commit.match(regex);
|
||||
|
||||
return matches && matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a branch name to extract some data.
|
||||
*
|
||||
* @param branch Branch name to parse.
|
||||
* @return Data.
|
||||
*/
|
||||
static parseBranch(branch) {
|
||||
const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i');
|
||||
|
||||
const matches = branch.match(regex);
|
||||
if (!matches || matches.length < 3) {
|
||||
throw new Error(`Error parsing branch ${branch}`);
|
||||
}
|
||||
|
||||
return {
|
||||
issue: matches[0],
|
||||
project: matches[1],
|
||||
issueNumber: matches[2],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Utils;
|
469
gulpfile.js
469
gulpfile.js
|
@ -1,437 +1,68 @@
|
|||
var gulp = require('gulp'),
|
||||
fs = require('fs'),
|
||||
through = require('through'),
|
||||
rename = require('gulp-rename'),
|
||||
path = require('path'),
|
||||
slash = require('gulp-slash'),
|
||||
clipEmptyFiles = require('gulp-clip-empty-files'),
|
||||
File = require('vinyl'),
|
||||
flatten = require('gulp-flatten'),
|
||||
npmPath = require('path'),
|
||||
concat = require('gulp-concat'),
|
||||
htmlmin = require('gulp-htmlmin'),
|
||||
bufferFrom = require('buffer-from'),
|
||||
exec = require('child_process').exec,
|
||||
license = '' +
|
||||
'// (C) Copyright 2015 Moodle Pty Ltd.\n' +
|
||||
'//\n' +
|
||||
'// Licensed under the Apache License, Version 2.0 (the "License");\n' +
|
||||
'// you may not use this file except in compliance with the License.\n' +
|
||||
'// You may obtain a copy of the License at\n' +
|
||||
'//\n' +
|
||||
'// http://www.apache.org/licenses/LICENSE-2.0\n' +
|
||||
'//\n' +
|
||||
'// Unless required by applicable law or agreed to in writing, software\n' +
|
||||
'// distributed under the License is distributed on an "AS IS" BASIS,\n' +
|
||||
'// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' +
|
||||
'// See the License for the specific language governing permissions and\n' +
|
||||
'// limitations under the License.\n\n';
|
||||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Copy a property from one object to another, adding a prefix to the key if needed.
|
||||
* @param {Object} target Object to copy the properties to.
|
||||
* @param {Object} source Object to copy the properties from.
|
||||
* @param {String} prefix Prefix to add to the keys.
|
||||
*/
|
||||
function addProperties(target, source, prefix) {
|
||||
for (var property in source) {
|
||||
target[prefix + property] = source[property];
|
||||
}
|
||||
}
|
||||
const BuildConfigTask = require('./gulp/task-build-config');
|
||||
const BuildLangTask = require('./gulp/task-build-lang');
|
||||
const CombineScssTask = require('./gulp/task-combine-scss');
|
||||
const CopyComponentTemplatesTask = require('./gulp/task-copy-component-templates');
|
||||
const PushTask = require('./gulp/task-push');
|
||||
const Utils = require('./gulp/utils');
|
||||
const gulp = require('gulp');
|
||||
const pathLib = require('path');
|
||||
|
||||
/**
|
||||
* Treats a file to merge JSONs. This function is based on gulp-jsoncombine module.
|
||||
* https://github.com/reflog/gulp-jsoncombine
|
||||
* @param {Object} file File treated.
|
||||
*/
|
||||
function treatFile(file, data) {
|
||||
if (file.isNull() || file.isStream()) {
|
||||
return; // ignore
|
||||
}
|
||||
try {
|
||||
var srcPos = file.path.lastIndexOf('/src/');
|
||||
if (srcPos == -1) {
|
||||
// It's probably a Windows environment.
|
||||
srcPos = file.path.lastIndexOf('\\src\\');
|
||||
}
|
||||
const paths = {
|
||||
lang: [
|
||||
'./src/lang/',
|
||||
'./src/core/**/lang/',
|
||||
'./src/addon/**/lang/',
|
||||
'./src/assets/countries/',
|
||||
'./src/assets/mimetypes/'
|
||||
],
|
||||
config: './src/config.json',
|
||||
};
|
||||
|
||||
var path = file.path.substr(srcPos + 5);
|
||||
data[path] = JSON.parse(file.contents.toString());
|
||||
} catch (err) {
|
||||
console.log('Error parsing JSON: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Treats the merged JSON data, adding prefixes depending on the component. Used in lang tasks.
|
||||
*
|
||||
* @param {Object} data Merged data.
|
||||
* @return {Buffer} Buffer with the treated data.
|
||||
*/
|
||||
function treatMergedData(data) {
|
||||
var merged = {};
|
||||
var mergedOrdered = {};
|
||||
|
||||
for (var filepath in data) {
|
||||
var pathSplit = filepath.split(/[\/\\]/),
|
||||
prefix;
|
||||
|
||||
pathSplit.pop();
|
||||
|
||||
switch (pathSplit[0]) {
|
||||
case 'lang':
|
||||
prefix = 'core';
|
||||
break;
|
||||
case 'core':
|
||||
if (pathSplit[1] == 'lang') {
|
||||
// Not used right now.
|
||||
prefix = 'core';
|
||||
} else {
|
||||
prefix = 'core.' + pathSplit[1];
|
||||
}
|
||||
break;
|
||||
case 'addon':
|
||||
// Remove final item 'lang'.
|
||||
pathSplit.pop();
|
||||
// Remove first item 'addon'.
|
||||
pathSplit.shift();
|
||||
|
||||
// For subplugins. We'll use plugin_subfolder_subfolder2_...
|
||||
// E.g. 'mod_assign_feedback_comments'.
|
||||
prefix = 'addon.' + pathSplit.join('_');
|
||||
break;
|
||||
case 'assets':
|
||||
prefix = 'assets.' + pathSplit[1];
|
||||
break;
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
addProperties(merged, data[filepath], prefix + '.');
|
||||
}
|
||||
}
|
||||
|
||||
// Force ordering by string key.
|
||||
Object.keys(merged).sort().forEach(function(k){
|
||||
mergedOrdered[k] = merged[k];
|
||||
});
|
||||
|
||||
return bufferFrom(JSON.stringify(mergedOrdered, null, 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build lang file.
|
||||
*
|
||||
* @param {String} language Language to translate.
|
||||
* @param {String[]} langPaths Paths to the possible language files.
|
||||
* @param {String} buildDest Path where to leave the built files.
|
||||
* @param {Function} done Function to call when done.
|
||||
* @return {Void}
|
||||
*/
|
||||
function buildLang(language, langPaths, buildDest, done) {
|
||||
var filename = language + '.json',
|
||||
data = {},
|
||||
firstFile = null;
|
||||
|
||||
var paths = langPaths.map(function(path) {
|
||||
if (path.slice(-1) != '/') {
|
||||
path = path + '/';
|
||||
}
|
||||
return path + language + '.json';
|
||||
});
|
||||
|
||||
gulp.src(paths, { allowEmpty: true })
|
||||
.pipe(slash())
|
||||
.pipe(clipEmptyFiles())
|
||||
.pipe(through(function(file) {
|
||||
if (!firstFile) {
|
||||
firstFile = file;
|
||||
}
|
||||
return treatFile(file, data);
|
||||
}, function() {
|
||||
/* This implementation is based on gulp-jsoncombine module.
|
||||
* https://github.com/reflog/gulp-jsoncombine */
|
||||
if (firstFile) {
|
||||
var joinedPath = path.join(firstFile.base, language+'.json');
|
||||
|
||||
var joinedFile = new File({
|
||||
cwd: firstFile.cwd,
|
||||
base: firstFile.base,
|
||||
path: joinedPath,
|
||||
contents: treatMergedData(data)
|
||||
});
|
||||
|
||||
this.emit('data', joinedFile);
|
||||
}
|
||||
this.emit('end');
|
||||
}))
|
||||
.pipe(gulp.dest(buildDest))
|
||||
.on('end', done);
|
||||
}
|
||||
|
||||
// Delete a folder and all its contents.
|
||||
function deleteFolderRecursive(path) {
|
||||
if (fs.existsSync(path)) {
|
||||
fs.readdirSync(path).forEach(function(file) {
|
||||
var curPath = npmPath.join(path, file);
|
||||
if (fs.lstatSync(curPath).isDirectory()) {
|
||||
deleteFolderRecursive(curPath);
|
||||
} else {
|
||||
fs.unlinkSync(curPath);
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmdirSync(path);
|
||||
}
|
||||
}
|
||||
|
||||
// List of app lang files. To be used only if cannot get it from filesystem.
|
||||
var paths = {
|
||||
src: './src',
|
||||
assets: './src/assets',
|
||||
lang: [
|
||||
'./src/lang/',
|
||||
'./src/core/**/lang/',
|
||||
'./src/addon/**/lang/',
|
||||
'./src/assets/countries/',
|
||||
'./src/assets/mimetypes/'
|
||||
],
|
||||
config: './src/config.json',
|
||||
};
|
||||
const args = Utils.getCommandLineArguments();
|
||||
|
||||
// Build the language files into a single file per language.
|
||||
gulp.task('lang', function(done) {
|
||||
buildLang('en', paths.lang, path.join(paths.assets, 'lang'), done);
|
||||
gulp.task('lang', (done) => {
|
||||
new BuildLangTask().run('en', paths.lang, done);
|
||||
});
|
||||
|
||||
// Convert config.json into a TypeScript class.
|
||||
gulp.task('config', function(done) {
|
||||
// Get the last commit.
|
||||
exec('git log -1 --pretty=format:"%H"', function (err, commit, stderr) {
|
||||
if (err) {
|
||||
console.error('An error occurred while getting the last commit: ' + err);
|
||||
} else if (stderr) {
|
||||
console.error('An error occurred while getting the last commit: ' + stderr);
|
||||
}
|
||||
gulp.task('config', (done) => {
|
||||
new BuildConfigTask().run(paths.config, done);
|
||||
});
|
||||
|
||||
gulp.src(paths.config)
|
||||
.pipe(through(function(file) {
|
||||
// Convert the contents of the file into a TypeScript class.
|
||||
// Disable the rule variable-name in the file.
|
||||
var config = JSON.parse(file.contents.toString()),
|
||||
contents = license + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n',
|
||||
that = this;
|
||||
// Copy component templates to www to make compile-html work in AOT.
|
||||
gulp.task('copy-component-templates', (done) => {
|
||||
new CopyComponentTemplatesTask().run(done);
|
||||
});
|
||||
|
||||
for (var key in config) {
|
||||
var value = config[key];
|
||||
if (typeof value == 'string') {
|
||||
// Wrap the string in ' and scape them.
|
||||
value = "'" + value.replace(/([^\\])'/g, "$1\\'") + "'";
|
||||
} else if (typeof value != 'number' && typeof value != 'boolean') {
|
||||
// Stringify with 4 spaces of indentation, and then add 4 more spaces in each line.
|
||||
value = JSON.stringify(value, null, 4).replace(/^(?: )/gm, ' ').replace(/^(?:})/gm, ' }');
|
||||
// Replace " by ' in values.
|
||||
value = value.replace(/: "([^"]*)"/g, ": '$1'");
|
||||
// Combine SCSS files.
|
||||
gulp.task('combine-scss', (done) => {
|
||||
new CombineScssTask().run(done);
|
||||
});
|
||||
|
||||
// Check if the keys have "-" in it.
|
||||
var matches = value.match(/"([^"]*\-[^"]*)":/g);
|
||||
if (matches) {
|
||||
// Replace " by ' in keys. We cannot remove them because keys have chars like '-'.
|
||||
value = value.replace(/"([^"]*)":/g, "'$1':");
|
||||
} else {
|
||||
// Remove ' in keys.
|
||||
value = value.replace(/"([^"]*)":/g, "$1:");
|
||||
}
|
||||
|
||||
// Add type any to the key.
|
||||
key = key + ': any';
|
||||
}
|
||||
|
||||
// If key has quotation marks, remove them.
|
||||
if (key[0] == '"') {
|
||||
key = key.substr(1, key.length - 2);
|
||||
}
|
||||
contents += ' static ' + key + ' = ' + value + ';\n';
|
||||
}
|
||||
|
||||
// Add compilation info.
|
||||
contents += ' static compilationtime = ' + Date.now() + ';\n';
|
||||
contents += ' static lastcommit = \'' + commit + '\';\n';
|
||||
|
||||
contents += '}\n';
|
||||
|
||||
file.contents = bufferFrom(contents);
|
||||
this.emit('data', file);
|
||||
}))
|
||||
.pipe(rename('configconstants.ts'))
|
||||
.pipe(gulp.dest(paths.src))
|
||||
.on('end', done);
|
||||
});
|
||||
gulp.task('push', (done) => {
|
||||
new PushTask().run(args, done);
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.parallel('lang', 'config'));
|
||||
|
||||
gulp.task('watch', function() {
|
||||
var langsPaths = paths.lang.map(function(path) {
|
||||
return path + 'en.json';
|
||||
});
|
||||
gulp.task('watch', () => {
|
||||
const langsPaths = paths.lang.map(path => path + 'en.json');
|
||||
|
||||
gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang'));
|
||||
gulp.watch(paths.config, { interval: 500 }, gulp.parallel('config'));
|
||||
});
|
||||
|
||||
var templatesSrc = [
|
||||
'./src/components/**/*.html',
|
||||
'./src/core/**/components/**/*.html',
|
||||
'./src/core/**/component/**/*.html',
|
||||
// Copy all addon components because any component can be injected using extraImports.
|
||||
'./src/addon/**/components/**/*.html',
|
||||
'./src/addon/**/component/**/*.html'
|
||||
],
|
||||
templatesDest = './www/templates';
|
||||
|
||||
// Copy component templates to www to make compile-html work in AOT.
|
||||
gulp.task('copy-component-templates', function(done) {
|
||||
deleteFolderRecursive(templatesDest);
|
||||
|
||||
gulp.src(templatesSrc, { allowEmpty: true })
|
||||
.pipe(flatten())
|
||||
// Check options here: https://github.com/kangax/html-minifier
|
||||
.pipe(htmlmin({
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
caseSensitive: true
|
||||
}))
|
||||
.pipe(gulp.dest(templatesDest))
|
||||
.on('end', done);
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds the file and returns its content.
|
||||
*
|
||||
* @param {string} capture Import file path.
|
||||
* @param {string} baseDir Directory where the file was found.
|
||||
* @param {string} paths Alternative paths where to find the imports.
|
||||
* @param {Array} parsedFiles Yet parsed files to reduce size of the result.
|
||||
* @return {string} Partially combined scss.
|
||||
*/
|
||||
function getReplace(capture, baseDir, paths, parsedFiles) {
|
||||
var parse = path.parse(path.resolve(baseDir, capture + '.scss'));
|
||||
var file = parse.dir + '/' + parse.name;
|
||||
|
||||
if (file.slice(-3) === '.wp') {
|
||||
console.log('Windows Phone not supported "' + capture);
|
||||
// File was already parsed, leave the import commented.
|
||||
return '// @import "' + capture + '";';
|
||||
}
|
||||
|
||||
if (!fs.existsSync(file + '.scss')) {
|
||||
// File not found, might be a partial file.
|
||||
file = parse.dir + '/_' + parse.name;
|
||||
}
|
||||
|
||||
// If file still not found, try to find the file in the alternative paths.
|
||||
var x = 0;
|
||||
while (!fs.existsSync(file + '.scss') && paths.length > x) {
|
||||
parse = path.parse(path.resolve(paths[x], capture + '.scss'));
|
||||
file = parse.dir + '/' + parse.name;
|
||||
|
||||
x++;
|
||||
}
|
||||
|
||||
file = file + '.scss';
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
// File not found. Leave the import there.
|
||||
console.log('File "' + capture + '" not found');
|
||||
return '@import "' + capture + '";';
|
||||
}
|
||||
|
||||
if (parsedFiles.indexOf(file) >= 0) {
|
||||
console.log('File "' + capture + '" already parsed');
|
||||
// File was already parsed, leave the import commented.
|
||||
return '// @import "' + capture + '";';
|
||||
}
|
||||
|
||||
parsedFiles.push(file);
|
||||
var text = fs.readFileSync(file);
|
||||
|
||||
// Recursive call.
|
||||
return scssCombine(text, parse.dir, paths, parsedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine scss files with its imports
|
||||
*
|
||||
* @param {string} content Scss string to read.
|
||||
* @param {string} baseDir Directory where the file was found.
|
||||
* @param {string} paths Alternative paths where to find the imports.
|
||||
* @param {Array} parsedFiles Yet parsed files to reduce size of the result.
|
||||
* @return {string} Scss string with the replaces done.
|
||||
*/
|
||||
function scssCombine(content, baseDir, paths, parsedFiles) {
|
||||
|
||||
// Content is a Buffer, convert to string.
|
||||
if (typeof content != "string") {
|
||||
content = content.toString();
|
||||
}
|
||||
|
||||
// Search of single imports.
|
||||
var regex = /@import[ ]*['"](.*)['"][ ]*;/g;
|
||||
|
||||
if (regex.test(content)) {
|
||||
return content.replace(regex, function(m, capture) {
|
||||
if (capture == "bmma") {
|
||||
return m;
|
||||
}
|
||||
|
||||
return getReplace(capture, baseDir, paths, parsedFiles);
|
||||
});
|
||||
}
|
||||
|
||||
// Search of multiple imports.
|
||||
regex = /@import(?:[ \n]+['"](.*)['"][,]?[ \n]*)+;/gm;
|
||||
if (regex.test(content)) {
|
||||
return content.replace(regex, function(m, capture) {
|
||||
var text = "";
|
||||
|
||||
// Divide the import into multiple files.
|
||||
regex = /['"]([^'"]*)['"]/g;
|
||||
var captures = m.match(regex);
|
||||
for (var x in captures) {
|
||||
text += getReplace(captures[x].replace(/['"]+/g, ''), baseDir, paths, parsedFiles) + "\n";
|
||||
}
|
||||
|
||||
return text;
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
gulp.task('combine-scss', function(done) {
|
||||
var paths = [
|
||||
'node_modules/ionic-angular/themes/',
|
||||
'node_modules/font-awesome/scss/',
|
||||
'node_modules/ionicons/dist/scss/'
|
||||
];
|
||||
|
||||
var parsedFiles = [];
|
||||
|
||||
gulp.src([
|
||||
'./src/theme/variables.scss',
|
||||
'./node_modules/ionic-angular/themes/ionic.globals.*.scss',
|
||||
'./node_modules/ionic-angular/themes/ionic.components.scss',
|
||||
'./src/**/*.scss']) // define a source files
|
||||
.pipe(through(function(file, encoding, callback) {
|
||||
if (file.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
parsedFiles.push(file);
|
||||
file.contents = bufferFrom(scssCombine(file.contents, path.dirname(file.path), paths, parsedFiles));
|
||||
|
||||
this.emit('data', file);
|
||||
})) // combine them based on @import and save it to stream
|
||||
.pipe(concat('combined.scss')) // concat the stream output in single file
|
||||
.pipe(gulp.dest('.')) // save file to destination.
|
||||
.on('end', done);
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load Diff
90
package.json
90
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "moodlemobile",
|
||||
"version": "3.9.2",
|
||||
"version": "3.9.3",
|
||||
"description": "The official app for Moodle.",
|
||||
"author": {
|
||||
"name": "Moodle Pty Ltd.",
|
||||
|
@ -57,19 +57,21 @@
|
|||
"@angular/platform-browser-dynamic": "5.2.11",
|
||||
"@ionic-native/badge": "4.20.0",
|
||||
"@ionic-native/camera": "4.20.0",
|
||||
"@ionic-native/chooser": "^4.20.0",
|
||||
"@ionic-native/chooser": "4.20.0",
|
||||
"@ionic-native/clipboard": "4.20.0",
|
||||
"@ionic-native/core": "4.20.0",
|
||||
"@ionic-native/device": "4.20.0",
|
||||
"@ionic-native/diagnostic": "4.2.0",
|
||||
"@ionic-native/file": "4.20.0",
|
||||
"@ionic-native/file-opener": "4.20.0",
|
||||
"@ionic-native/file-transfer": "4.20.0",
|
||||
"@ionic-native/geolocation": "4.20.0",
|
||||
"@ionic-native/globalization": "4.20.0",
|
||||
"@ionic-native/http": "^4.20.0",
|
||||
"@ionic-native/http": "4.20.0",
|
||||
"@ionic-native/in-app-browser": "4.20.0",
|
||||
"@ionic-native/keyboard": "4.20.0",
|
||||
"@ionic-native/local-notifications": "4.20.0",
|
||||
"@ionic-native/media": "4.20.0",
|
||||
"@ionic-native/media-capture": "4.20.0",
|
||||
"@ionic-native/network": "4.20.0",
|
||||
"@ionic-native/push": "4.20.0",
|
||||
|
@ -85,7 +87,7 @@
|
|||
"ajv": "6.11.0",
|
||||
"chart.js": "2.9.3",
|
||||
"com-darryncampbell-cordova-plugin-intent": "1.3.0",
|
||||
"cordova": "9.0.0",
|
||||
"cordova": "10.0.0",
|
||||
"cordova-android": "8.1.0",
|
||||
"cordova-android-support-gradle-release": "3.0.1",
|
||||
"cordova-clipboard": "1.3.0",
|
||||
|
@ -93,23 +95,24 @@
|
|||
"cordova-plugin-advanced-http": "2.4.1",
|
||||
"cordova-plugin-badge": "0.8.8",
|
||||
"cordova-plugin-camera": "4.1.0",
|
||||
"cordova-plugin-chooser": "1.3.1",
|
||||
"cordova-plugin-customurlscheme": "5.0.0",
|
||||
"cordova-plugin-chooser": "1.3.2",
|
||||
"cordova-plugin-customurlscheme": "5.0.1",
|
||||
"cordova-plugin-device": "2.0.3",
|
||||
"cordova-plugin-file": "6.0.2",
|
||||
"cordova-plugin-file-opener2": "3.0.0",
|
||||
"cordova-plugin-file-opener2": "3.0.4",
|
||||
"cordova-plugin-file-transfer": "1.7.1",
|
||||
"cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff",
|
||||
"cordova-plugin-globalization": "1.11.0",
|
||||
"cordova-plugin-inappbrowser": "4.0.0",
|
||||
"cordova-plugin-inappbrowser": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle",
|
||||
"cordova-plugin-ionic-keyboard": "2.1.3",
|
||||
"cordova-plugin-ionic-webview": "4.1.3",
|
||||
"cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle",
|
||||
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
|
||||
"cordova-plugin-media": "5.0.3",
|
||||
"cordova-plugin-media-capture": "3.0.3",
|
||||
"cordova-plugin-network-information": "2.0.2",
|
||||
"cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist",
|
||||
"cordova-plugin-screen-orientation": "3.0.2",
|
||||
"cordova-plugin-splashscreen": "5.0.3",
|
||||
"cordova-plugin-splashscreen": "6.0.0",
|
||||
"cordova-plugin-statusbar": "2.4.3",
|
||||
"cordova-plugin-whitelist": "1.3.4",
|
||||
"cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git",
|
||||
|
@ -119,6 +122,7 @@
|
|||
"cordova-support-google-services": "1.3.2",
|
||||
"es6-promise-plugin": "4.2.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"inquirer": "^7.3.2",
|
||||
"ionic-angular": "3.9.9",
|
||||
"ionicons": "3.0.0",
|
||||
"jszip": "3.1.5",
|
||||
|
@ -136,31 +140,36 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@ionic/app-scripts": "3.2.3",
|
||||
"@ionic/cli": "^6.9.3",
|
||||
"@types/cordova": "0.0.34",
|
||||
"@types/cordova-plugin-file-transfer": "0.0.3",
|
||||
"@types/cordova-plugin-globalization": "0.0.3",
|
||||
"@types/cordova-plugin-network-information": "0.0.3",
|
||||
"@types/node": "8.10.59",
|
||||
"@types/promise.prototype.finally": "2.0.4",
|
||||
"@ionic/cli": "^6.11.7",
|
||||
"@types/cordova": "^0.0.34",
|
||||
"@types/cordova-plugin-file-transfer": "^0.0.3",
|
||||
"@types/cordova-plugin-globalization": "^0.0.3",
|
||||
"@types/cordova-plugin-network-information": "^0.0.3",
|
||||
"@types/node": "^8.10.59",
|
||||
"@types/promise.prototype.finally": "^2.0.4",
|
||||
"acorn": "^5.7.4",
|
||||
"electron-builder-lib": "20.23.1",
|
||||
"electron-rebuild": "1.10.0",
|
||||
"cordova.plugins.diagnostic": "^5.0.2",
|
||||
"electron-builder-lib": "^20.23.1",
|
||||
"electron-rebuild": "^1.10.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-clip-empty-files": "0.1.2",
|
||||
"gulp-concat": "2.6.1",
|
||||
"gulp-flatten": "0.4.0",
|
||||
"gulp-htmlmin": "5.0.1",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-slash": "1.1.3",
|
||||
"lodash.template": "4.5.0",
|
||||
"gulp-clip-empty-files": "^0.1.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-flatten": "^0.4.0",
|
||||
"gulp-htmlmin": "^5.0.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-slash": "^1.1.3",
|
||||
"lodash.template": "^4.5.0",
|
||||
"minimist": "^1.2.5",
|
||||
"native-run": "^1.0.0",
|
||||
"node-loader": "0.6.0",
|
||||
"through": "2.3.8",
|
||||
"typescript": "2.6.2",
|
||||
"vinyl": "2.2.0",
|
||||
"webpack-merge": "4.2.2"
|
||||
"node-loader": "^0.6.0",
|
||||
"request": "^2.88.2",
|
||||
"through": "^2.3.8",
|
||||
"typescript": "~2.6.2",
|
||||
"vinyl": "^2.2.0",
|
||||
"webpack-merge": "^4.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"keytar": "^6.0.1"
|
||||
},
|
||||
"browser": {
|
||||
"electron": false
|
||||
|
@ -213,7 +222,11 @@
|
|||
"cordova-plugin-wkwebview-cookies": {},
|
||||
"cordova-plugin-qrscanner": {},
|
||||
"cordova-plugin-chooser": {},
|
||||
"cordova-plugin-wkuserscript": {}
|
||||
"cordova-plugin-wkuserscript": {},
|
||||
"cordova-plugin-media": {
|
||||
"KEEP_AVAUDIOSESSION_ALWAYS_ACTIVE": "NO"
|
||||
},
|
||||
"cordova.plugins.diagnostic": {}
|
||||
}
|
||||
},
|
||||
"main": "desktop/electron.js",
|
||||
|
@ -248,14 +261,19 @@
|
|||
}
|
||||
],
|
||||
"compression": "maximum",
|
||||
"electronVersion": "4.2.5",
|
||||
"electronVersion": "8.0.2",
|
||||
"mac": {
|
||||
"category": "public.app-category.education",
|
||||
"icon": "resources/desktop/icon.icns",
|
||||
"target": "mas",
|
||||
"bundleVersion": "3.9.2",
|
||||
"bundleVersion": "3.9.3",
|
||||
"extendInfo": {
|
||||
"ElectronTeamID": "2NU57U5PAW"
|
||||
"ElectronTeamID": "2NU57U5PAW",
|
||||
"NSLocationWhenInUseUsageDescription": "We need your location so you can attach it as part of your submissions.",
|
||||
"NSLocationAlwaysUsageDescription": "We need your location so you can attach it as part of your submissions.",
|
||||
"NSCameraUsageDescription": "We need camera access to take pictures so you can attach them as part of your submissions.",
|
||||
"NSMicrophoneUsageDescription": "We need microphone access to record sounds so you can attach them as part of your submissions.",
|
||||
"NSPhotoLibraryUsageDescription": "We need photo library access to get pictures from there so you can attach them as part of your submissions."
|
||||
}
|
||||
},
|
||||
"win": {
|
||||
|
@ -274,6 +292,6 @@
|
|||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": "11.x"
|
||||
"node": ">=11.x"
|
||||
}
|
||||
}
|
|
@ -258,7 +258,10 @@ function parse_file {
|
|||
value=`$exec`
|
||||
guess_file $key "$value"
|
||||
else
|
||||
if [ ! -z "$findbetter" ]; then
|
||||
if [ "$found" == 'donottranslate' ]; then
|
||||
# Do nothing since is not translatable.
|
||||
continue
|
||||
elif [ ! -z "$findbetter" ]; then
|
||||
exec="jq -r .\"$key\" $1"
|
||||
value=`$exec`
|
||||
find_better_file "$key" "$value" "$found"
|
||||
|
|
|
@ -210,6 +210,18 @@ function build_lang($lang, $keys) {
|
|||
$string = get_translation_strings($langfoldername, $value->file, $override_langfolder);
|
||||
// Apply translations.
|
||||
if (!$string) {
|
||||
if ($value->file == 'donottranslate') {
|
||||
// Restore it form the json.
|
||||
if ($langFile && is_array($langFile) && isset($langFile[$key])) {
|
||||
$translations[$key] = $langFile[$key];
|
||||
} else {
|
||||
// If not present, do not count it in the total.
|
||||
$total--;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TOTRANSLATE) {
|
||||
echo "\n\t\tTo translate $value->string on $value->file";
|
||||
}
|
||||
|
@ -312,6 +324,10 @@ function detect_lang($lang, $keys) {
|
|||
$string = get_translation_strings($langfoldername, $value->file);
|
||||
// Apply translations.
|
||||
if (!$string) {
|
||||
// Do not count non translatable in the totals.
|
||||
if ($value->file == 'donottranslate') {
|
||||
$total--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
"addon.block_myoverview.hiddencourses": "block_myoverview",
|
||||
"addon.block_myoverview.inprogress": "block_myoverview",
|
||||
"addon.block_myoverview.lastaccessed": "block_myoverview",
|
||||
"addon.block_myoverview.morecourses": "block_myoverview",
|
||||
"addon.block_myoverview.nocourses": "block_myoverview",
|
||||
"addon.block_myoverview.past": "block_myoverview",
|
||||
"addon.block_myoverview.pluginname": "block_myoverview",
|
||||
|
@ -408,6 +407,7 @@
|
|||
"addon.mod_assign.submitassignment_help": "assign",
|
||||
"addon.mod_assign.submittedearly": "assign",
|
||||
"addon.mod_assign.submittedlate": "assign",
|
||||
"addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp",
|
||||
"addon.mod_assign.timemodified": "assign",
|
||||
"addon.mod_assign.timeremaining": "assign",
|
||||
"addon.mod_assign.ungroupedusers": "assign",
|
||||
|
@ -463,6 +463,7 @@
|
|||
"addon.mod_choice.errorgetchoice": "local_moodlemobileapp",
|
||||
"addon.mod_choice.expired": "choice",
|
||||
"addon.mod_choice.full": "choice",
|
||||
"addon.mod_choice.limita": "choice",
|
||||
"addon.mod_choice.modulenameplural": "choice",
|
||||
"addon.mod_choice.noresultsviewable": "choice",
|
||||
"addon.mod_choice.notopenyet": "choice",
|
||||
|
@ -476,6 +477,7 @@
|
|||
"addon.mod_choice.publishinfonever": "choice",
|
||||
"addon.mod_choice.removemychoice": "choice",
|
||||
"addon.mod_choice.responses": "choice",
|
||||
"addon.mod_choice.responsesa": "choice",
|
||||
"addon.mod_choice.responsesresultgraphdescription": "local_moodlemobileapp",
|
||||
"addon.mod_choice.responsesresultgraphheader": "choice",
|
||||
"addon.mod_choice.resultsnotsynced": "local_moodlemobileapp",
|
||||
|
@ -505,11 +507,13 @@
|
|||
"addon.mod_data.foundrecords": "data",
|
||||
"addon.mod_data.gettinglocation": "local_moodlemobileapp",
|
||||
"addon.mod_data.latlongboth": "data",
|
||||
"addon.mod_data.locationnotenabled": "local_moodlemobileapp",
|
||||
"addon.mod_data.locationpermissiondenied": "local_moodlemobileapp",
|
||||
"addon.mod_data.menuchoose": "data",
|
||||
"addon.mod_data.modulenameplural": "data",
|
||||
"addon.mod_data.more": "data",
|
||||
"addon.mod_data.mylocation": "local_moodlemobileapp",
|
||||
"addon.mod_data.noaccess": "data",
|
||||
"addon.mod_data.nomatch": "data",
|
||||
"addon.mod_data.norecords": "data",
|
||||
"addon.mod_data.notapproved": "data",
|
||||
|
@ -1370,6 +1374,8 @@
|
|||
"core.cannotconnecttrouble": "local_moodlemobileapp",
|
||||
"core.cannotconnectverify": "local_moodlemobileapp",
|
||||
"core.cannotdownloadfiles": "local_moodlemobileapp",
|
||||
"core.cannotopeninapp": "local_moodlemobileapp",
|
||||
"core.cannotopeninappdownload": "local_moodlemobileapp",
|
||||
"core.captureaudio": "local_moodlemobileapp",
|
||||
"core.capturedimage": "local_moodlemobileapp",
|
||||
"core.captureimage": "local_moodlemobileapp",
|
||||
|
@ -1378,6 +1384,7 @@
|
|||
"core.choose": "moodle",
|
||||
"core.choosedots": "moodle",
|
||||
"core.clearsearch": "local_moodlemobileapp",
|
||||
"core.clearstoreddata": "local_moodlemobileapp",
|
||||
"core.clicktohideshow": "moodle",
|
||||
"core.clicktoseefull": "local_moodlemobileapp",
|
||||
"core.close": "repository",
|
||||
|
@ -1431,6 +1438,7 @@
|
|||
"core.course.availablespace": "local_moodlemobileapp",
|
||||
"core.course.cannotdeletewhiledownloading": "local_moodlemobileapp",
|
||||
"core.course.confirmdeletemodulefiles": "local_moodlemobileapp",
|
||||
"core.course.confirmdeletestoreddata": "local_moodlemobileapp",
|
||||
"core.course.confirmdownload": "local_moodlemobileapp",
|
||||
"core.course.confirmdownloadunknownsize": "local_moodlemobileapp",
|
||||
"core.course.confirmdownloadzerosize": "local_moodlemobileapp",
|
||||
|
@ -1522,6 +1530,7 @@
|
|||
"core.done": "survey",
|
||||
"core.download": "moodle",
|
||||
"core.downloaded": "local_moodlemobileapp",
|
||||
"core.downloadfile": "moodle",
|
||||
"core.downloading": "local_moodlemobileapp",
|
||||
"core.edit": "moodle",
|
||||
"core.editor.autosavesucceeded": "editor_atto",
|
||||
|
@ -1557,6 +1566,7 @@
|
|||
"core.errorsomedatanotdownloaded": "local_moodlemobileapp",
|
||||
"core.errorsync": "local_moodlemobileapp",
|
||||
"core.errorsyncblocked": "local_moodlemobileapp",
|
||||
"core.errorurlschemeinvalidscheme": "local_moodlemobileapp",
|
||||
"core.errorurlschemeinvalidsite": "local_moodlemobileapp",
|
||||
"core.explanationdigitalminor": "moodle",
|
||||
"core.favourites": "moodle",
|
||||
|
@ -1750,6 +1760,7 @@
|
|||
"core.login.erroraccesscontrolalloworigin": "local_moodlemobileapp",
|
||||
"core.login.errordeletesite": "local_moodlemobileapp",
|
||||
"core.login.errorexampleurl": "local_moodlemobileapp",
|
||||
"core.login.errorqrnoscheme": "local_moodlemobileapp",
|
||||
"core.login.errorupdatesite": "local_moodlemobileapp",
|
||||
"core.login.faqcannotconnectanswer": "local_moodlemobileapp",
|
||||
"core.login.faqcannotconnectquestion": "local_moodlemobileapp",
|
||||
|
@ -1826,7 +1837,9 @@
|
|||
"core.login.selectacountry": "moodle",
|
||||
"core.login.selectsite": "local_moodlemobileapp",
|
||||
"core.login.signupplugindisabled": "local_moodlemobileapp",
|
||||
"core.login.signuprequiredfieldnotsupported": "local_moodlemobileapp",
|
||||
"core.login.siteaddress": "local_moodlemobileapp",
|
||||
"core.login.siteaddressplaceholder": "donottranslate",
|
||||
"core.login.sitehasredirect": "local_moodlemobileapp",
|
||||
"core.login.siteinmaintenance": "local_moodlemobileapp",
|
||||
"core.login.sitepolicynotagreederror": "local_moodlemobileapp",
|
||||
|
@ -1848,6 +1861,7 @@
|
|||
"core.mainmenu.help": "moodle",
|
||||
"core.mainmenu.logout": "moodle",
|
||||
"core.mainmenu.website": "local_moodlemobileapp",
|
||||
"core.maxfilesize": "moodle",
|
||||
"core.maxsizeandattachments": "moodle",
|
||||
"core.min": "moodle",
|
||||
"core.mins": "moodle",
|
||||
|
@ -1899,6 +1913,7 @@
|
|||
"core.noresults": "moodle",
|
||||
"core.noselection": "form",
|
||||
"core.notapplicable": "local_moodlemobileapp",
|
||||
"core.notavailable": "moodle",
|
||||
"core.notenrolledprofile": "moodle",
|
||||
"core.notice": "moodle",
|
||||
"core.notingroup": "moodle",
|
||||
|
@ -1909,6 +1924,7 @@
|
|||
"core.offline": "message",
|
||||
"core.ok": "moodle",
|
||||
"core.online": "message",
|
||||
"core.openfile": "local_moodlemobileapp",
|
||||
"core.openfullimage": "local_moodlemobileapp",
|
||||
"core.openinbrowser": "local_moodlemobileapp",
|
||||
"core.openmodinbrowser": "local_moodlemobileapp",
|
||||
|
@ -1929,8 +1945,8 @@
|
|||
"core.question.certainty": "qbehaviour_deferredcbm",
|
||||
"core.question.complete": "question",
|
||||
"core.question.correct": "question",
|
||||
"core.question.errorattachmentsnotsupported": "local_moodlemobileapp",
|
||||
"core.question.errorinlinefilesnotsupported": "local_moodlemobileapp",
|
||||
"core.question.errorattachmentsnotsupportedinsite": "local_moodlemobileapp",
|
||||
"core.question.errorembeddedfilesnotsupportedinsite": "local_moodlemobileapp",
|
||||
"core.question.errorquestionnotsupported": "local_moodlemobileapp",
|
||||
"core.question.feedback": "question",
|
||||
"core.question.howtodraganddrop": "local_moodlemobileapp",
|
||||
|
@ -1981,6 +1997,7 @@
|
|||
"core.settings.about": "local_moodlemobileapp",
|
||||
"core.settings.appsettings": "local_moodlemobileapp",
|
||||
"core.settings.appversion": "local_moodlemobileapp",
|
||||
"core.settings.cannotsyncloggedout": "local_moodlemobileapp",
|
||||
"core.settings.cannotsyncoffline": "local_moodlemobileapp",
|
||||
"core.settings.cannotsyncwithoutwifi": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme": "local_moodlemobileapp",
|
||||
|
|
|
@ -17,16 +17,7 @@ export MOODLE_DOCKER_APP_PATH=$basedir
|
|||
print_title "Preparing dependencies"
|
||||
git clone --branch master --depth 1 git://github.com/moodle/moodle $HOME/moodle
|
||||
git clone --branch master --depth 1 git://github.com/moodlehq/moodle-local_moodlemobileapp $HOME/moodle/local/moodlemobileapp
|
||||
# git clone --branch master --depth 1 git://github.com/moodlehq/moodle-docker $HOME/moodle-docker
|
||||
|
||||
# TODO replace with commented line above once https://github.com/moodlehq/moodle-docker/pull/126 is merged
|
||||
mkdir $HOME/moodle-docker
|
||||
cd $HOME/moodle-docker
|
||||
git init
|
||||
git remote add origin git://github.com/moodlehq/moodle-docker
|
||||
git fetch --depth 1 origin c604d5f9792c72fb9d83f6fec1f4b1defd778e9a
|
||||
git checkout FETCH_HEAD
|
||||
cd -
|
||||
git clone --branch master --depth 1 git://github.com/moodlehq/moodle-docker $HOME/moodle-docker
|
||||
|
||||
cp $HOME/moodle-docker/config.docker-template.php $HOME/moodle/config.php
|
||||
|
||||
|
@ -50,7 +41,7 @@ print_title "Running e2e tests"
|
|||
# Run tests
|
||||
for tags in "$@"
|
||||
do
|
||||
$dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags=\"$tags\""
|
||||
$dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags=\"$tags\" --auto-rerun"
|
||||
notify_on_error_exit "Some e2e tests are failing, please review"
|
||||
done
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<div class="safe-area-page">
|
||||
<ion-grid no-padding>
|
||||
<ion-row no-padding>
|
||||
<ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-4 align-self-stretch>
|
||||
<ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-3 align-self-stretch>
|
||||
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true" [showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
|
|
@ -77,6 +77,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
downloadCourseEnabled: boolean;
|
||||
downloadCoursesEnabled: boolean;
|
||||
|
||||
protected FILTER_PRIORITY = ['all', 'allincludinghidden', 'inprogress', 'future', 'past', 'favourite', 'hidden', 'custom'];
|
||||
protected prefetchIconsInitialized = false;
|
||||
protected isDestroyed;
|
||||
protected coursesObserver;
|
||||
|
@ -202,9 +203,6 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
|
||||
this.initCourseFilters(courses);
|
||||
|
||||
this.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 ||
|
||||
typeof courses[0].enddate != 'undefined');
|
||||
|
||||
this.courses.filter = '';
|
||||
this.showFilter = false;
|
||||
|
||||
|
@ -250,10 +248,15 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
this.showSelectorFilter = Object.keys(this.showFilters).some((key) => {
|
||||
return this.showFilters[key] == 'show';
|
||||
});
|
||||
|
||||
if (!this.showSelectorFilter) {
|
||||
// All filters disabled, display all the courses.
|
||||
this.showFilters.all = 'show';
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.showSelectorFilter || (this.selectedFilter === 'inprogress' && this.showFilters.inprogress == 'disabled')) {
|
||||
// No selector, or the default option is disabled, show all.
|
||||
if (!this.showSelectorFilter) {
|
||||
// No selector, display all the courses.
|
||||
this.selectedFilter = 'all';
|
||||
}
|
||||
this.setCourseFilter(this.selectedFilter);
|
||||
|
@ -388,7 +391,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
if (this.showFilters[filter] == 'show') {
|
||||
this.filteredCourses = this.courses[filter];
|
||||
} else {
|
||||
const activeFilter = Object.keys(this.showFilters).find((name) => {
|
||||
const activeFilter = this.FILTER_PRIORITY.find((name) => {
|
||||
return this.showFilters[name] == 'show';
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
"hiddencourses": "Removed from view",
|
||||
"inprogress": "In progress",
|
||||
"lastaccessed": "Last accessed",
|
||||
"morecourses": "More courses",
|
||||
"nocourses": "No courses",
|
||||
"past": "Past",
|
||||
"pluginname": "Course overview",
|
||||
|
|
|
@ -64,12 +64,12 @@
|
|||
</a>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
<!-- Create a calendar event. -->
|
||||
<ion-fab core-fab bottom end *ngIf="canCreate">
|
||||
<button ion-fab (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</button>
|
||||
</ion-fab>
|
||||
</core-loading>
|
||||
|
||||
<!-- Create a calendar event. -->
|
||||
<ion-fab core-fab bottom end *ngIf="canCreate && loaded">
|
||||
<button ion-fab (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
|
|
@ -1606,61 +1606,55 @@ export class AddonCalendarProvider {
|
|||
* @param siteId Site ID the event belongs to. If not defined, use current site.
|
||||
* @return Promise resolved when the notification is scheduled.
|
||||
*/
|
||||
protected scheduleEventNotification(event: AddonCalendarAnyEvent, reminderId: number, time: number, siteId?: string)
|
||||
protected async scheduleEventNotification(event: AddonCalendarAnyEvent, reminderId: number, time: number, siteId?: string)
|
||||
: Promise<void> {
|
||||
|
||||
if (this.localNotificationsProvider.isAvailable()) {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
if (!this.localNotificationsProvider.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (time === 0) {
|
||||
// Cancel if it was scheduled.
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
if (time === 0) {
|
||||
// Cancel if it was scheduled.
|
||||
return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
|
||||
}
|
||||
|
||||
if (time == -1) {
|
||||
// If time is -1, get event default time to calculate the notification time.
|
||||
time = await this.getDefaultNotificationTime(siteId);
|
||||
|
||||
if (time == 0) {
|
||||
// Default notification time is disabled, do not show.
|
||||
return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
|
||||
}
|
||||
|
||||
let promise;
|
||||
if (time == -1) {
|
||||
// If time is -1, get event default time to calculate the notification time.
|
||||
promise = this.getDefaultNotificationTime(siteId).then((time) => {
|
||||
if (time == 0) {
|
||||
// Default notification time is disabled, do not show.
|
||||
return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
|
||||
}
|
||||
|
||||
return event.timestart - (time * 60);
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve(time);
|
||||
}
|
||||
|
||||
return promise.then((time) => {
|
||||
time = time * 1000;
|
||||
|
||||
if (time <= new Date().getTime()) {
|
||||
// This reminder is over, don't schedule. Cancel if it was scheduled.
|
||||
return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
|
||||
}
|
||||
|
||||
const notification: ILocalNotification = {
|
||||
id: reminderId,
|
||||
title: event.name,
|
||||
text: this.timeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true),
|
||||
icon: 'file://assets/img/icons/calendar.png',
|
||||
trigger: {
|
||||
at: new Date(time)
|
||||
},
|
||||
data: {
|
||||
eventid: event.id,
|
||||
reminderid: reminderId,
|
||||
siteid: siteId
|
||||
}
|
||||
};
|
||||
|
||||
return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId);
|
||||
});
|
||||
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
time = event.timestart - (time * 60);
|
||||
}
|
||||
|
||||
time = time * 1000;
|
||||
|
||||
if (time <= Date.now()) {
|
||||
// This reminder is over, don't schedule. Cancel if it was scheduled.
|
||||
return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
|
||||
}
|
||||
|
||||
const notification: ILocalNotification = {
|
||||
id: reminderId,
|
||||
title: event.name,
|
||||
text: this.timeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true),
|
||||
icon: 'file://assets/img/icons/calendar.png',
|
||||
trigger: {
|
||||
at: new Date(time)
|
||||
},
|
||||
data: {
|
||||
eventid: event.id,
|
||||
reminderid: reminderId,
|
||||
siteid: siteId
|
||||
}
|
||||
};
|
||||
|
||||
return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -62,6 +62,39 @@ export class AddonCompetencyProvider {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether current user can see another user competencies in a course.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param userId User ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with boolean: whether the user can view the competencies.
|
||||
*/
|
||||
canViewUserCompetenciesInCourse(courseId: number, userId?: number, siteId?: string): Promise<boolean> {
|
||||
if (!this.sitesProvider.isLoggedIn()) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return this.getCourseCompetenciesPage(courseId, siteId).then((response) => {
|
||||
if (!response.competencies.length) {
|
||||
// No competencies.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!userId || userId == this.sitesProvider.getCurrentSiteUserId()) {
|
||||
// Current user.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if current user can view any competency of the user.
|
||||
return this.getCompetencyInCourse(courseId, response.competencies[0].competency.id, userId, siteId).then(() => {
|
||||
return true;
|
||||
});
|
||||
}).catch(() => {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for user learning plans data WS calls.
|
||||
*
|
||||
|
@ -333,7 +366,7 @@ export class AddonCompetencyProvider {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get all competencies in a course.
|
||||
* Get all competencies in a course for a certain user.
|
||||
*
|
||||
* @param courseId ID of the course.
|
||||
* @param userId ID of the user.
|
||||
|
@ -344,6 +377,39 @@ export class AddonCompetencyProvider {
|
|||
getCourseCompetencies(courseId: number, userId?: number, siteId?: string, ignoreCache?: boolean)
|
||||
: Promise<AddonCompetencyDataForCourseCompetenciesPageResult> {
|
||||
|
||||
return this.getCourseCompetenciesPage(courseId, siteId, ignoreCache).then((response) => {
|
||||
|
||||
if (!userId || userId == this.sitesProvider.getCurrentSiteUserId()) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let promises: Promise<AddonCompetencyUserCompetencySummaryInCourse>[];
|
||||
|
||||
promises = response.competencies.map((competency) =>
|
||||
this.getCompetencyInCourse(courseId, competency.competency.id, userId, siteId)
|
||||
);
|
||||
|
||||
return Promise.all(promises).then((responses: AddonCompetencyUserCompetencySummaryInCourse[]) => {
|
||||
responses.forEach((resp, index) => {
|
||||
response.competencies[index].usercompetencycourse = resp.usercompetencysummary.usercompetencycourse;
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all competencies in a course.
|
||||
*
|
||||
* @param courseId ID of the course.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @return Promise to be resolved when the course competencies are retrieved.
|
||||
*/
|
||||
getCourseCompetenciesPage(courseId: number, siteId?: string, ignoreCache?: boolean)
|
||||
: Promise<AddonCompetencyDataForCourseCompetenciesPageResult> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
|
||||
this.logger.debug('Get course competencies for course ' + courseId);
|
||||
|
@ -370,26 +436,6 @@ export class AddonCompetencyProvider {
|
|||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
|
||||
}).then((response) => {
|
||||
|
||||
if (!userId || userId == this.sitesProvider.getCurrentSiteUserId()) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let promises: Promise<AddonCompetencyUserCompetencySummaryInCourse>[];
|
||||
|
||||
promises = response.competencies.map((competency) =>
|
||||
this.getCompetencyInCourse(courseId, competency.competency.id, userId, siteId)
|
||||
);
|
||||
|
||||
return Promise.all(promises).then((responses: AddonCompetencyUserCompetencySummaryInCourse[]) => {
|
||||
responses.forEach((resp, index) => {
|
||||
response.competencies[index].usercompetencycourse = resp.usercompetencysummary.usercompetencycourse;
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -71,15 +71,10 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler {
|
|||
return this.participantsNavEnabledCache[cacheKey];
|
||||
}
|
||||
|
||||
return this.competencyProvider.getCourseCompetencies(courseId, user.id).then((response) => {
|
||||
const enabled = response.competencies.length > 0;
|
||||
return this.competencyProvider.canViewUserCompetenciesInCourse(courseId, user.id).then((enabled) => {
|
||||
this.participantsNavEnabledCache[cacheKey] = enabled;
|
||||
|
||||
return enabled;
|
||||
}).catch((message) => {
|
||||
this.participantsNavEnabledCache[cacheKey] = false;
|
||||
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
// Link on a user site profile.
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate';
|
||||
import { AddonMessageOutputAirnotifierProvider } from './providers/airnotifier';
|
||||
import { AddonMessageOutputAirnotifierHandler } from './providers/handler';
|
||||
|
@ -28,7 +29,13 @@ import { AddonMessageOutputAirnotifierHandler } from './providers/handler';
|
|||
]
|
||||
})
|
||||
export class AddonMessageOutputAirnotifierModule {
|
||||
constructor(messageOutputDelegate: AddonMessageOutputDelegate, airnotifierHandler: AddonMessageOutputAirnotifierHandler) {
|
||||
constructor(messageOutputDelegate: AddonMessageOutputDelegate, airnotifierHandler: AddonMessageOutputAirnotifierHandler,
|
||||
eventsProvider: CoreEventsProvider, airnotifierProvider: AddonMessageOutputAirnotifierProvider) {
|
||||
messageOutputDelegate.registerHandler(airnotifierHandler);
|
||||
|
||||
eventsProvider.on(CoreEventsProvider.DEVICE_REGISTERED_IN_MOODLE, async (data) => {
|
||||
// Get user devices to make Moodle send the devices data to Airnotifier.
|
||||
airnotifierProvider.getUserDevices(true, data.siteId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<ion-list>
|
||||
<ion-item text-wrap *ngFor="let device of devices">
|
||||
<ion-label [class.core-bold]="device.current">
|
||||
{{ device.model }}
|
||||
{{ device.name }} {{ device.model }} {{ device.platform }} {{ device.version }}
|
||||
<span *ngIf="device.current">({{ 'core.currentdevice' | translate }})</span>
|
||||
</ion-label>
|
||||
<ion-spinner *ngIf="device.updating" item-end></ion-spinner>
|
||||
|
|
|
@ -74,10 +74,11 @@ export class AddonMessageOutputAirnotifierProvider {
|
|||
/**
|
||||
* Get user devices.
|
||||
*
|
||||
* @param ignoreCache Whether to ignore cache.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved with the devices.
|
||||
*/
|
||||
getUserDevices(siteId?: string): Promise<AddonMessageOutputAirnotifierDevice[]> {
|
||||
getUserDevices(ignoreCache?: boolean, siteId?: string): Promise<AddonMessageOutputAirnotifierDevice[]> {
|
||||
this.logger.debug('Get user devices');
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
|
@ -89,6 +90,11 @@ export class AddonMessageOutputAirnotifierProvider {
|
|||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
|
||||
return site.read('message_airnotifier_get_user_devices', data, preSets)
|
||||
.then((data: AddonMessageOutputAirnotifierGetUserDevicesResult) => {
|
||||
return data.devices;
|
||||
|
|
|
@ -581,35 +581,38 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
* @param offset Offset for message list.
|
||||
* @return Promise resolved with the list of messages.
|
||||
*/
|
||||
protected getConversationMessages(pagesToLoad: number, offset: number = 0)
|
||||
protected async getConversationMessages(pagesToLoad: number, offset: number = 0)
|
||||
: Promise<AddonMessagesConversationMessageFormatted[]> {
|
||||
|
||||
const excludePending = offset > 0;
|
||||
|
||||
return this.messagesProvider.getConversationMessages(this.conversationId, excludePending, offset).then((result) => {
|
||||
pagesToLoad--;
|
||||
|
||||
// Treat members. Don't use CoreUtilsProvider.arrayToObject because we don't want to override the existing object.
|
||||
if (result.members) {
|
||||
result.members.forEach((member) => {
|
||||
this.members[member.id] = member;
|
||||
});
|
||||
}
|
||||
|
||||
if (pagesToLoad > 0 && result.canLoadMore) {
|
||||
offset += AddonMessagesProvider.LIMIT_MESSAGES;
|
||||
|
||||
// Get more messages.
|
||||
return this.getConversationMessages(pagesToLoad, offset).then((nextMessages) => {
|
||||
return result.messages.concat(nextMessages);
|
||||
});
|
||||
} else {
|
||||
// No more messages to load, return them.
|
||||
this.canLoadMore = result.canLoadMore;
|
||||
|
||||
return result.messages;
|
||||
}
|
||||
const result = await this.messagesProvider.getConversationMessages(this.conversationId, {
|
||||
excludePending: excludePending,
|
||||
limitFrom: offset,
|
||||
});
|
||||
|
||||
pagesToLoad--;
|
||||
|
||||
// Treat members. Don't use CoreUtilsProvider.arrayToObject because we don't want to override the existing object.
|
||||
if (result.members) {
|
||||
result.members.forEach((member) => {
|
||||
this.members[member.id] = member;
|
||||
});
|
||||
}
|
||||
|
||||
if (pagesToLoad > 0 && result.canLoadMore) {
|
||||
offset += AddonMessagesProvider.LIMIT_MESSAGES;
|
||||
|
||||
// Get more messages.
|
||||
const nextMessages = await this.getConversationMessages(pagesToLoad, offset);
|
||||
|
||||
return result.messages.concat(nextMessages);
|
||||
} else {
|
||||
// No more messages to load, return them.
|
||||
this.canLoadMore = result.canLoadMore;
|
||||
|
||||
return result.messages;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,7 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
|
|||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreWSExternalWarning } from '@providers/ws';
|
||||
|
||||
/**
|
||||
|
@ -398,7 +398,7 @@ export class AddonMessagesProvider {
|
|||
* @param userId The other person with whom the current user is having the discussion.
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getCacheKeyForDiscussion(userId: number): string {
|
||||
getCacheKeyForDiscussion(userId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'discussion:' + userId;
|
||||
}
|
||||
|
||||
|
@ -889,93 +889,92 @@ export class AddonMessagesProvider {
|
|||
* Get a conversation by the conversation ID.
|
||||
*
|
||||
* @param conversationId Conversation ID to fetch.
|
||||
* @param excludePending True to exclude messages pending to be sent.
|
||||
* @param limitFrom Offset for messages list.
|
||||
* @param limitTo Limit of messages.
|
||||
* @param newestFirst Whether to order messages by newest first.
|
||||
* @param timeFrom The timestamp from which the messages were created.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @param userId User ID. If not defined, current user in the site.
|
||||
* @param options Options.
|
||||
* @return Promise resolved with the response.
|
||||
* @since 3.6
|
||||
*/
|
||||
getConversationMessages(conversationId: number, excludePending: boolean, limitFrom: number = 0, limitTo?: number,
|
||||
newestFirst: boolean = true, timeFrom: number = 0, siteId?: string, userId?: number)
|
||||
async getConversationMessages(conversationId: number, options?: AddonMessagesGetConversationMessagesOptions)
|
||||
: Promise<AddonMessagesGetConversationMessagesResult> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
options = options || {};
|
||||
|
||||
if (typeof limitTo == 'undefined' || limitTo === null) {
|
||||
limitTo = this.LIMIT_MESSAGES;
|
||||
const site = await this.sitesProvider.getSite(options.siteId);
|
||||
|
||||
options.userId = options.userId || site.getUserId();
|
||||
options.limitFrom = options.limitFrom || 0;
|
||||
options.limitTo = options.limitTo === undefined || options.limitTo === null ? this.LIMIT_MESSAGES : options.limitTo;
|
||||
options.timeFrom = options.timeFrom || 0;
|
||||
options.newestFirst = options.newestFirst === undefined || options.newestFirst === null ? true : options.newestFirst;
|
||||
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getCacheKeyForConversationMessages(options.userId, conversationId),
|
||||
};
|
||||
const params = {
|
||||
currentuserid: options.userId,
|
||||
convid: conversationId,
|
||||
limitfrom: options.limitFrom,
|
||||
limitnum: options.limitTo < 1 ? options.limitTo : options.limitTo + 1, // If there's a limit, get 1 more than requested.
|
||||
newest: options.newestFirst ? 1 : 0,
|
||||
timefrom: options.timeFrom,
|
||||
};
|
||||
|
||||
if (options.limitFrom > 0) {
|
||||
// Do not use cache when retrieving older messages.
|
||||
// This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded.
|
||||
preSets.getFromCache = false;
|
||||
preSets.saveToCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
} else if (options.forceCache) {
|
||||
preSets.omitExpires = true;
|
||||
} else if (options.ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
const result: AddonMessagesGetConversationMessagesResult =
|
||||
await site.read('core_message_get_conversation_messages', params, preSets);
|
||||
|
||||
if (options.limitTo < 1) {
|
||||
result.canLoadMore = false;
|
||||
result.messages = result.messages;
|
||||
} else {
|
||||
result.canLoadMore = result.messages.length > options.limitTo;
|
||||
result.messages = result.messages.slice(0, options.limitTo);
|
||||
}
|
||||
|
||||
let lastReceived;
|
||||
|
||||
result.messages.forEach((message) => {
|
||||
// Convert time to milliseconds.
|
||||
message.timecreated = message.timecreated ? message.timecreated * 1000 : 0;
|
||||
|
||||
if (!lastReceived && message.useridfrom != options.userId) {
|
||||
lastReceived = message;
|
||||
}
|
||||
|
||||
const preSets = {
|
||||
cacheKey: this.getCacheKeyForConversationMessages(userId, conversationId)
|
||||
},
|
||||
params: any = {
|
||||
currentuserid: userId,
|
||||
convid: conversationId,
|
||||
limitfrom: limitFrom,
|
||||
limitnum: limitTo < 1 ? limitTo : limitTo + 1, // If there is a limit, get 1 more than requested.
|
||||
newest: newestFirst ? 1 : 0,
|
||||
timefrom: timeFrom
|
||||
};
|
||||
|
||||
if (limitFrom > 0) {
|
||||
// Do not use cache when retrieving older messages.
|
||||
// This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded.
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['saveToCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
|
||||
return site.read('core_message_get_conversation_messages', params, preSets)
|
||||
.then((result: AddonMessagesGetConversationMessagesResult) => {
|
||||
|
||||
if (limitTo < 1) {
|
||||
result.canLoadMore = false;
|
||||
result.messages = result.messages;
|
||||
} else {
|
||||
result.canLoadMore = result.messages.length > limitTo;
|
||||
result.messages = result.messages.slice(0, limitTo);
|
||||
}
|
||||
|
||||
let lastReceived;
|
||||
|
||||
result.messages.forEach((message) => {
|
||||
// Convert time to milliseconds.
|
||||
message.timecreated = message.timecreated ? message.timecreated * 1000 : 0;
|
||||
|
||||
if (!lastReceived && message.useridfrom != userId) {
|
||||
lastReceived = message;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.appProvider.isDesktop() && limitFrom === 0 && lastReceived) {
|
||||
// Store the last received message (we cannot know if it's unread or not). Don't block the user for this.
|
||||
this.storeLastReceivedMessageIfNeeded(conversationId, lastReceived, site.getId());
|
||||
}
|
||||
|
||||
if (excludePending) {
|
||||
// No need to get offline messages, return the ones we have.
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get offline messages.
|
||||
return this.messagesOffline.getConversationMessages(conversationId).then((offlineMessages) => {
|
||||
// Mark offline messages as pending.
|
||||
offlineMessages.forEach((message) => {
|
||||
message.pending = true;
|
||||
message.useridfrom = userId;
|
||||
});
|
||||
|
||||
result.messages = result.messages.concat(offlineMessages);
|
||||
|
||||
return result;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (this.appProvider.isDesktop() && options.limitFrom === 0 && lastReceived) {
|
||||
// Store the last received message (we cannot know if it's unread or not). Don't block the user for this.
|
||||
this.storeLastReceivedMessageIfNeeded(conversationId, lastReceived, site.getId());
|
||||
}
|
||||
|
||||
if (options.excludePending) {
|
||||
// No need to get offline messages, return the ones we have.
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get offline messages.
|
||||
const offlineMessages = await this.messagesOffline.getConversationMessages(conversationId);
|
||||
|
||||
// Mark offline messages as pending.
|
||||
offlineMessages.forEach((message) => {
|
||||
message.pending = true;
|
||||
message.useridfrom = options.userId;
|
||||
});
|
||||
|
||||
result.messages = result.messages.concat(offlineMessages);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1412,7 +1411,7 @@ export class AddonMessagesProvider {
|
|||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved with the data.
|
||||
*/
|
||||
protected getRecentMessages(params: any, preSets: any, limitFromUnread: number = 0, limitFromRead: number = 0,
|
||||
getRecentMessages(params: any, preSets: any, limitFromUnread: number = 0, limitFromRead: number = 0,
|
||||
toDisplay: boolean = true, siteId?: string): Promise<AddonMessagesGetMessagesMessage[]> {
|
||||
limitFromUnread = limitFromUnread || 0;
|
||||
limitFromRead = limitFromRead || 0;
|
||||
|
@ -1962,7 +1961,8 @@ export class AddonMessagesProvider {
|
|||
* @since 3.2
|
||||
*/
|
||||
isMarkAllMessagesReadEnabled(): boolean {
|
||||
return this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_messages_as_read');
|
||||
return this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_conversation_messages_as_read') ||
|
||||
this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_messages_as_read');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2787,6 +2787,21 @@ export class AddonMessagesProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to pass to getConversationMessages.
|
||||
*/
|
||||
export type AddonMessagesGetConversationMessagesOptions = {
|
||||
excludePending?: boolean; // True to exclude messages pending to be sent.
|
||||
limitFrom?: number; // Offset for messages list. Defaults to 0.
|
||||
limitTo?: number; // Limit of messages.
|
||||
newestFirst?: boolean; // Whether to order messages by newest first.
|
||||
timeFrom?: number; // The timestamp from which the messages were created (in seconds). Defaults to 0.
|
||||
siteId?: string; // Site ID. If not defined, use current site.
|
||||
userId?: number; // User ID. If not defined, current user in the site.
|
||||
forceCache?: boolean; // True if it should return cached data. Has priority over ignoreCache.
|
||||
ignoreCache?: boolean; // True if it should ignore cached data (it will always fail in offline or server down).
|
||||
};
|
||||
|
||||
/**
|
||||
* Conversation.
|
||||
*/
|
||||
|
|
|
@ -32,8 +32,10 @@ export class AddonMessagesSettingsHandler implements CoreSettingsHandler {
|
|||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return this.messagesProvider.isMessagePreferencesEnabled();
|
||||
async isEnabled(): Promise<boolean> {
|
||||
const messagingEnabled = await this.messagesProvider.isPluginEnabled();
|
||||
|
||||
return messagingEnabled && this.messagesProvider.isMessagePreferencesEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,6 +26,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
|||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
|
||||
/**
|
||||
* Service to sync messages.
|
||||
|
@ -134,112 +135,165 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
|
|||
*
|
||||
* @param conversationId Conversation ID.
|
||||
* @param userId User ID talking to (if no conversation ID).
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved with the list of warnings if sync is successful, rejected otherwise.
|
||||
*/
|
||||
syncDiscussion(conversationId: number, userId: number, siteId?: string): Promise<any> {
|
||||
syncDiscussion(conversationId: number, userId: number, siteId?: string): Promise<string[]> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const syncId = this.getSyncId(conversationId, userId),
|
||||
groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled();
|
||||
const syncId = this.getSyncId(conversationId, userId);
|
||||
|
||||
if (this.isSyncing(syncId, siteId)) {
|
||||
// There's already a sync ongoing for this conversation, return the promise.
|
||||
return this.getOngoingSync(syncId, siteId);
|
||||
}
|
||||
|
||||
const warnings = [];
|
||||
return this.addOngoingSync(syncId, this.performSyncDiscussion(conversationId, userId, siteId), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the synchronization of a discussion.
|
||||
*
|
||||
* @param conversationId Conversation ID.
|
||||
* @param userId User ID talking to (if no conversation ID).
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved with the list of warnings if sync is successful, rejected otherwise.
|
||||
*/
|
||||
protected async performSyncDiscussion(conversationId: number, userId: number, siteId: string): Promise<string[]> {
|
||||
const groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled();
|
||||
let messages: any[];
|
||||
const errors = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (conversationId) {
|
||||
this.logger.debug(`Try to sync conversation '${conversationId}'`);
|
||||
messages = await this.messagesOffline.getConversationMessages(conversationId, siteId);
|
||||
} else {
|
||||
this.logger.debug(`Try to sync discussion with user '${userId}'`);
|
||||
messages = await this.messagesOffline.getMessages(userId, siteId);
|
||||
}
|
||||
|
||||
// Get offline messages to be sent.
|
||||
let syncPromise;
|
||||
if (!messages.length) {
|
||||
// Nothing to sync.
|
||||
return [];
|
||||
} else if (!this.appProvider.isOnline()) {
|
||||
// Cannot sync in offline. Mark messages as device offline.
|
||||
this.messagesOffline.setMessagesDeviceOffline(messages, true);
|
||||
|
||||
if (conversationId) {
|
||||
syncPromise = this.messagesOffline.getConversationMessages(conversationId, siteId);
|
||||
} else {
|
||||
syncPromise = this.messagesOffline.getMessages(userId, siteId);
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
syncPromise = syncPromise.then((messages) => {
|
||||
if (!messages.length) {
|
||||
// Nothing to sync.
|
||||
return [];
|
||||
} else if (!this.appProvider.isOnline()) {
|
||||
// Cannot sync in offline. Mark messages as device offline.
|
||||
this.messagesOffline.setMessagesDeviceOffline(messages, true);
|
||||
// Order message by timecreated.
|
||||
messages = this.messagesProvider.sortMessages(messages);
|
||||
|
||||
return Promise.reject(null);
|
||||
}
|
||||
// Get messages sent by the user after the first offline message was sent.
|
||||
// We subtract some time because the message could've been saved in server before it was in the app.
|
||||
const timeFrom = Math.floor((messages[0].timecreated - CoreConstants.WS_TIMEOUT - 1000) / 1000);
|
||||
const onlineMessages = await this.getMessagesSentAfter(timeFrom, conversationId, userId, siteId);
|
||||
|
||||
let promise: Promise<any> = Promise.resolve();
|
||||
const errors = [];
|
||||
// Send the messages. Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed.
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
const textFieldName = conversationId ? 'text' : 'smallmessage';
|
||||
const wrappedText = message[textFieldName][0] != '<' ? '<p>' + message[textFieldName] + '</p>' : message[textFieldName];
|
||||
|
||||
// Order message by timecreated.
|
||||
messages = this.messagesProvider.sortMessages(messages);
|
||||
|
||||
// Send the messages.
|
||||
// Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed.
|
||||
messages.forEach((message, index) => {
|
||||
// Chain message sending. If 1 message fails to be sent we'll stop sending.
|
||||
promise = promise.then(() => {
|
||||
let subPromise;
|
||||
|
||||
if (conversationId) {
|
||||
subPromise = this.messagesProvider.sendMessageToConversationOnline(conversationId, message.text, siteId);
|
||||
} else {
|
||||
subPromise = this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId);
|
||||
try {
|
||||
if (onlineMessages.indexOf(wrappedText) != -1) {
|
||||
// Message already sent, ignore it to prevent duplicates.
|
||||
} else if (conversationId) {
|
||||
await this.messagesProvider.sendMessageToConversationOnline(conversationId, message.text, siteId);
|
||||
} else {
|
||||
await this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!this.utils.isWebServiceError(error)) {
|
||||
// Error sending, stop execution.
|
||||
if (this.appProvider.isOnline()) {
|
||||
// App is online, unmark deviceoffline if marked.
|
||||
this.messagesOffline.setMessagesDeviceOffline(messages, false);
|
||||
}
|
||||
|
||||
return subPromise.catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// Error returned by WS. Store the error to show a warning but keep sending messages.
|
||||
if (errors.indexOf(error) == -1) {
|
||||
errors.push(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
// Error returned by WS. Store the error to show a warning but keep sending messages.
|
||||
if (errors.indexOf(error) == -1) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Error sending, stop execution.
|
||||
if (this.appProvider.isOnline()) {
|
||||
// App is online, unmark deviceoffline if marked.
|
||||
this.messagesOffline.setMessagesDeviceOffline(messages, false);
|
||||
}
|
||||
// Message was sent, delete it from local DB.
|
||||
if (conversationId) {
|
||||
await this.messagesOffline.deleteConversationMessage(conversationId, message.text, message.timecreated, siteId);
|
||||
} else {
|
||||
await this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}).then(() => {
|
||||
// Message was sent, delete it from local DB.
|
||||
if (conversationId) {
|
||||
return this.messagesOffline.deleteConversationMessage(conversationId, message.text,
|
||||
message.timecreated, siteId);
|
||||
} else {
|
||||
return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId);
|
||||
}
|
||||
}).then(() => {
|
||||
// In some Moodle versions, wait 1 second to make sure timecreated is different.
|
||||
// This is because there was a bug where messages with the same timecreated had a wrong order.
|
||||
if (!groupMessagingEnabled && index < messages.length - 1) {
|
||||
return new Promise((resolve, reject): any => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
// In some Moodle versions, wait 1 second to make sure timecreated is different.
|
||||
// This is because there was a bug where messages with the same timecreated had a wrong order.
|
||||
if (!groupMessagingEnabled && i < messages.length - 1) {
|
||||
await this.utils.wait(1000);
|
||||
}
|
||||
}
|
||||
|
||||
await this.handleSyncErrors(conversationId, userId, errors, warnings);
|
||||
|
||||
// All done, return the warnings.
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages sent by current user after a certain time.
|
||||
*
|
||||
* @param time Time in seconds.
|
||||
* @param conversationId Conversation ID.
|
||||
* @param userId User ID talking to (if no conversation ID).
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved with the messages texts.
|
||||
*/
|
||||
protected async getMessagesSentAfter(time: number, conversationId: number, userId: number, siteId: string): Promise<string[]> {
|
||||
const site = await this.sitesProvider.getSite(siteId);
|
||||
|
||||
const siteCurrentUserId = site.getUserId();
|
||||
|
||||
if (conversationId) {
|
||||
try {
|
||||
const result = await this.messagesProvider.getConversationMessages(conversationId, {
|
||||
excludePending: true,
|
||||
ignoreCache: true,
|
||||
timeFrom: time,
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}).then((errors) => {
|
||||
return this.handleSyncErrors(conversationId, userId, errors, warnings);
|
||||
}).then(() => {
|
||||
// All done, return the warnings.
|
||||
return warnings;
|
||||
});
|
||||
const sentMessages = result.messages.filter((message) => message.useridfrom == siteCurrentUserId);
|
||||
|
||||
return this.addOngoingSync(syncId, syncPromise, siteId);
|
||||
return sentMessages.map((message) => message.text);
|
||||
} catch (error) {
|
||||
if (error && error.errorcode == 'invalidresponse') {
|
||||
// There's a bug in Moodle that causes this error if there are no new messages. Return empty array.
|
||||
return [];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
|
||||
const params = {
|
||||
useridto: userId,
|
||||
useridfrom: siteCurrentUserId,
|
||||
limitnum: AddonMessagesProvider.LIMIT_MESSAGES,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.messagesProvider.getCacheKeyForDiscussion(userId),
|
||||
ignoreCache: true,
|
||||
};
|
||||
|
||||
const messages = await this.messagesProvider.getRecentMessages(params, preSets, 0, 0, false, siteId);
|
||||
|
||||
time = time * 1000; // Convert to milliseconds.
|
||||
const messagesAfterTime = messages.filter((message) => message.timecreated >= time);
|
||||
|
||||
return messagesAfterTime.map((message) => message.text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -251,7 +305,7 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
|
|||
* @param warnings Array where to place the warnings.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected handleSyncErrors(conversationId: number, userId: number, errors: any[], warnings: any[]): Promise<any> {
|
||||
protected handleSyncErrors(conversationId: number, userId: number, errors: any[], warnings: string[]): Promise<any> {
|
||||
if (errors && errors.length) {
|
||||
if (conversationId) {
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
this.hasOffline = hasOffline;
|
||||
|
||||
// Get assignment submissions.
|
||||
return this.assignProvider.getSubmissions(this.assign.id).then((data) => {
|
||||
return this.assignProvider.getSubmissions(this.assign.id, {cmId: this.module.id}).then((data) => {
|
||||
const time = this.timeUtils.timestamp();
|
||||
|
||||
this.canViewAllSubmissions = data.canviewsubmissions;
|
||||
|
@ -217,7 +217,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
}
|
||||
|
||||
// Check if the user can view their own submission.
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id).then(() => {
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, {cmId: this.module.id}).then(() => {
|
||||
this.canViewOwnSubmission = true;
|
||||
}).catch((error) => {
|
||||
this.canViewOwnSubmission = false;
|
||||
|
@ -241,7 +241,10 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
setGroup(groupId: number): Promise<any> {
|
||||
this.group = groupId;
|
||||
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, undefined, this.group).then((response) => {
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, {
|
||||
groupId: this.group,
|
||||
cmId: this.module.id,
|
||||
}).then((response) => {
|
||||
this.summary = response.gradingsummary;
|
||||
if (typeof this.summary.warnofungroupedusers == 'boolean' && this.summary.warnofungroupedusers) {
|
||||
this.summary.warnofungroupedusers = 'ungroupedusers';
|
||||
|
@ -299,7 +302,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
*/
|
||||
protected hasSyncSucceed(result: any): boolean {
|
||||
if (result.updated) {
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh();
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh(false);
|
||||
}
|
||||
|
||||
return result.updated;
|
||||
|
@ -324,7 +327,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
}
|
||||
|
||||
return Promise.all(promises).finally(() => {
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh();
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</ion-item>
|
||||
|
||||
<!-- Tabs: see the submission or grade it. -->
|
||||
<core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true">
|
||||
<core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true" (ionChange)="tabSelected($event)">
|
||||
<!-- View the submission tab. -->
|
||||
<core-tab [title]="'addon.mod_assign.submission' | translate">
|
||||
<ng-template>
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Component, Input, OnInit, OnDestroy, ViewChild, Optional, ViewChildren,
|
|||
import { NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreEventsProvider, CoreEventObserver } from '@providers/events';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreLangProvider } from '@providers/lang';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
|
@ -35,7 +35,9 @@ import {
|
|||
} from '../../providers/assign';
|
||||
import { AddonModAssignHelperProvider } from '../../providers/helper';
|
||||
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
|
||||
import { AddonModAssignSync, AddonModAssignSyncProvider } from '../../providers/assign-sync';
|
||||
import { CoreTabsComponent } from '@components/tabs/tabs';
|
||||
import { CoreTabComponent } from '@components/tabs/tab';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin';
|
||||
|
||||
|
@ -107,6 +109,8 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
protected submissionStatusAvailable: boolean; // Whether we were able to retrieve the submission status.
|
||||
protected originalGrades: any = {}; // Object with the original grade data, to check for changes.
|
||||
protected isDestroyed: boolean; // Whether the component has been destroyed.
|
||||
protected syncObserver: CoreEventObserver;
|
||||
protected hasOfflineGrade = false;
|
||||
|
||||
constructor(protected navCtrl: NavController, protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider,
|
||||
sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected timeUtils: CoreTimeUtilsProvider,
|
||||
|
@ -129,7 +133,29 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
this.selectedTab = this.showGrade && this.showGrade !== 'false' ? 1 : 0;
|
||||
this.isSubmittedForGrading = !!this.submitId;
|
||||
|
||||
this.loadData();
|
||||
this.loadData(true);
|
||||
|
||||
// Refresh data if this assign is synchronized and it's grading.
|
||||
const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED];
|
||||
|
||||
this.syncObserver = this.eventsProvider.onMultiple(events, async (data) => {
|
||||
// Check that user is grading and this grade wasn't blocked when sync was performed.
|
||||
if (!this.loaded || !this.isGrading || data.gradesBlocked.indexOf(this.submitId) != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.context == 'submission' && data.submitId == this.submitId) {
|
||||
// Manual sync triggered by this same submission, ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't refresh if the user has modified some data.
|
||||
const hasDataToSave = await this.hasDataToSave();
|
||||
|
||||
if (!hasDataToSave) {
|
||||
this.invalidateAndRefresh(false);
|
||||
}
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -241,7 +267,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
}, this.siteId);
|
||||
} else {
|
||||
// Invalidate and refresh data to update this view.
|
||||
this.invalidateAndRefresh();
|
||||
this.invalidateAndRefresh(true);
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.error', true);
|
||||
|
@ -281,17 +307,23 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
/**
|
||||
* Check if there's data to save (grade).
|
||||
*
|
||||
* @param isSubmit Whether the user is about to submit the grade.
|
||||
* @return Promise resolved with boolean: whether there's data to save.
|
||||
*/
|
||||
protected hasDataToSave(): Promise<boolean> {
|
||||
protected async hasDataToSave(isSubmit?: boolean): Promise<boolean> {
|
||||
if (!this.canSaveGrades || !this.loaded) {
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSubmit && this.hasOfflineGrade) {
|
||||
// Always allow sending if the grade is saved in offline.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if numeric grade and toggles changed.
|
||||
if (this.originalGrades.grade != this.grade.grade || this.originalGrades.addAttempt != this.grade.addAttempt ||
|
||||
this.originalGrades.applyToAll != this.grade.applyToAll) {
|
||||
return Promise.resolve(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if outcomes changed.
|
||||
|
@ -301,20 +333,21 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
|
||||
if (this.originalGrades.outcomes[outcome.id] == 'undefined' ||
|
||||
this.originalGrades.outcomes[outcome.id] != outcome.selectedId) {
|
||||
return Promise.resolve(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.feedback && this.feedback.plugins) {
|
||||
return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId)
|
||||
.catch(() => {
|
||||
try {
|
||||
return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId);
|
||||
} catch (error) {
|
||||
// Error ocurred, consider there are no changes.
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -334,9 +367,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
/**
|
||||
* Invalidate and refresh data.
|
||||
*
|
||||
* @param sync Whether to try to synchronize data.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
invalidateAndRefresh(): Promise<any> {
|
||||
invalidateAndRefresh(sync?: boolean): Promise<any> {
|
||||
this.loaded = false;
|
||||
|
||||
const promises = [];
|
||||
|
@ -361,16 +395,17 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
return Promise.all(promises).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.loadData();
|
||||
return this.loadData(sync);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the data to render the submission.
|
||||
*
|
||||
* @param sync Whether to try to synchronize data.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected loadData(): Promise<any> {
|
||||
protected async loadData(sync?: boolean): Promise<any> {
|
||||
let isBlind = !!this.blindId;
|
||||
|
||||
this.previousAttempt = undefined;
|
||||
|
@ -381,44 +416,53 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
isBlind = false;
|
||||
}
|
||||
|
||||
// Get the assignment.
|
||||
return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => {
|
||||
const time = this.timeUtils.timestamp(),
|
||||
promises = [];
|
||||
try {
|
||||
// Get the assignment.
|
||||
this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId);
|
||||
|
||||
this.assign = assign;
|
||||
if (this.submitId != this.currentUserId && sync) {
|
||||
// Teacher viewing a student submission. Try to sync the assign, there could be offline grades stored.
|
||||
try {
|
||||
const result = await AddonModAssignSync.instance.syncAssign(this.assign.id);
|
||||
|
||||
if (assign.allowsubmissionsfromdate && assign.allowsubmissionsfromdate >= time) {
|
||||
this.fromDate = this.timeUtils.userDate(assign.allowsubmissionsfromdate * 1000);
|
||||
if (result && result.updated) {
|
||||
this.eventsProvider.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, {
|
||||
assignId: this.assign.id,
|
||||
warnings: result.warnings,
|
||||
gradesBlocked: result.gradesBlocked,
|
||||
context: 'submission',
|
||||
submitId: this.submitId,
|
||||
}, this.siteId);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors, probably user is offline or sync is blocked.
|
||||
}
|
||||
}
|
||||
|
||||
const time = this.timeUtils.timestamp();
|
||||
let promises = [];
|
||||
|
||||
if (this.assign.allowsubmissionsfromdate && this.assign.allowsubmissionsfromdate >= time) {
|
||||
this.fromDate = this.timeUtils.userDate(this.assign.allowsubmissionsfromdate * 1000);
|
||||
}
|
||||
|
||||
this.currentAttempt = 0;
|
||||
this.maxAttemptsText = this.translate.instant('addon.mod_assign.unlimitedattempts');
|
||||
this.blindMarking = this.isSubmittedForGrading && assign.blindmarking && !assign.revealidentities;
|
||||
this.blindMarking = this.isSubmittedForGrading && this.assign.blindmarking && !this.assign.revealidentities;
|
||||
|
||||
if (!this.blindMarking && this.submitId != this.currentUserId) {
|
||||
promises.push(this.userProvider.getProfile(this.submitId, this.courseId).then((profile) => {
|
||||
this.user = profile;
|
||||
}));
|
||||
promises.push(this.loadSubmissionUserProfile());
|
||||
}
|
||||
|
||||
// Check if there's any offline data for this submission.
|
||||
promises.push(this.assignOfflineProvider.getSubmission(assign.id, this.submitId).then((data) => {
|
||||
this.hasOffline = data && data.plugindata && Object.keys(data.plugindata).length > 0;
|
||||
this.submittedOffline = data && data.submitted;
|
||||
}).catch(() => {
|
||||
// No offline data found.
|
||||
this.hasOffline = false;
|
||||
this.submittedOffline = false;
|
||||
}));
|
||||
promises.push(this.loadSubmissionOfflineData());
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
// Get submission status.
|
||||
return this.assignProvider.getSubmissionStatusWithRetry(this.assign, this.submitId, undefined, isBlind);
|
||||
}).then((response) => {
|
||||
const response = await this.assignProvider.getSubmissionStatusWithRetry(this.assign, {userId: this.submitId, isBlind});
|
||||
|
||||
const promises = [];
|
||||
promises = [];
|
||||
|
||||
this.submissionStatusAvailable = true;
|
||||
this.lastAttempt = response.lastattempt;
|
||||
|
@ -450,16 +494,41 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
// Get the submission plugins that don't support editing.
|
||||
promises.push(this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins).then((list) => {
|
||||
this.unsupportedEditPlugins = list;
|
||||
}));
|
||||
promises.push(this.loadUnsupportedPlugins());
|
||||
|
||||
return Promise.all(promises);
|
||||
}).catch((error) => {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.');
|
||||
}).finally(() => {
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load profile of submission's user.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadSubmissionUserProfile(): Promise<void> {
|
||||
this.user = await this.userProvider.getProfile(this.submitId, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load offline data for the submission (not the submission grade).
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadSubmissionOfflineData(): Promise<void> {
|
||||
try {
|
||||
const data = await this.assignOfflineProvider.getSubmission(this.assign.id, this.submitId);
|
||||
|
||||
this.hasOffline = data && data.plugindata && Object.keys(data.plugindata).length > 0;
|
||||
this.submittedOffline = data && data.submitted;
|
||||
} catch (error) {
|
||||
// No offline data found.
|
||||
this.hasOffline = false;
|
||||
this.submittedOffline = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -537,11 +606,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
// Make sure outcomes is an array.
|
||||
gradeInfo.outcomes = gradeInfo.outcomes || [];
|
||||
|
||||
if (!this.isDestroyed) {
|
||||
// Block the assignment.
|
||||
this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
|
||||
}
|
||||
|
||||
// Treat the grade info.
|
||||
return this.treatGradeInfo();
|
||||
}).then(() => {
|
||||
|
@ -589,11 +653,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
return this.assignOfflineProvider.getSubmissionGrade(this.assign.id, this.submitId).catch(() => {
|
||||
// Grade not found.
|
||||
}).then((data) => {
|
||||
this.hasOfflineGrade = false;
|
||||
|
||||
// Load offline grades.
|
||||
if (data && (!feedback || !feedback.gradeddate || feedback.gradeddate < data.timemodified)) {
|
||||
// If grade has been modified from gradebook, do not use offline.
|
||||
if (this.grade.modified < data.timemodified) {
|
||||
this.hasOfflineGrade = true;
|
||||
this.grade.grade = !this.grade.scale ? this.utils.formatFloat(data.grade) : data.grade;
|
||||
this.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
|
||||
this.gradingColor = '';
|
||||
|
@ -627,6 +693,15 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the submission plugins that don't support editing.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadUnsupportedPlugins(): Promise<void> {
|
||||
this.unsupportedEditPlugins = await this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the submission status name and class.
|
||||
*
|
||||
|
@ -725,7 +800,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
*/
|
||||
submitGrade(): Promise<any> {
|
||||
// Check if there's something to be saved.
|
||||
return this.hasDataToSave().then((modified) => {
|
||||
return this.hasDataToSave(true).then((modified) => {
|
||||
if (!modified) {
|
||||
return;
|
||||
}
|
||||
|
@ -764,7 +839,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
return this.discardDrafts();
|
||||
}).finally(() => {
|
||||
// Invalidate and refresh data.
|
||||
this.invalidateAndRefresh();
|
||||
this.invalidateAndRefresh(true);
|
||||
|
||||
this.eventsProvider.trigger(AddonModAssignProvider.GRADED_EVENT, {
|
||||
assignmentId: this.assign.id,
|
||||
|
@ -921,7 +996,9 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
response.lastattempt.submissiongroupmemberswhoneedtosubmit.forEach((member) => {
|
||||
if (this.blindMarking) {
|
||||
// Users not blinded! (Moodle < 3.1.1, 3.2).
|
||||
promises.push(this.assignProvider.getAssignmentUserMappings(this.assign.id, member).then((blindId) => {
|
||||
promises.push(this.assignProvider.getAssignmentUserMappings(this.assign.id, member, {
|
||||
cmId: this.moduleId,
|
||||
}).then((blindId) => {
|
||||
this.membersToSubmit.push(blindId);
|
||||
}));
|
||||
} else {
|
||||
|
@ -952,15 +1029,42 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block or unblock the automatic sync of the user grade.
|
||||
*
|
||||
* @param block Whether to block or unblock.
|
||||
*/
|
||||
protected setGradeSyncBlocked(block?: boolean): void {
|
||||
if (this.isDestroyed || !this.assign || !this.isGrading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncId = AddonModAssignSync.instance.getGradeSyncId(this.assign.id, this.submitId);
|
||||
|
||||
if (block) {
|
||||
this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, syncId);
|
||||
} else {
|
||||
this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, syncId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A certain tab has been selected, either manually or automatically.
|
||||
*
|
||||
* @param tab The tab that was selected.
|
||||
*/
|
||||
tabSelected(tab: CoreTabComponent): void {
|
||||
// Block sync when selecting grade tab, unblock when leaving it.
|
||||
this.setGradeSyncBlocked(this.tabs.getIndex(tab) === 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.setGradeSyncBlocked(false);
|
||||
this.isDestroyed = true;
|
||||
|
||||
if (this.assign && this.isGrading) {
|
||||
this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
|
||||
}
|
||||
this.syncObserver && this.syncObserver.off();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@
|
|||
"submitassignment": "Submit assignment",
|
||||
"submittedearly": "Assignment was submitted {{$a}} early",
|
||||
"submittedlate": "Assignment was submitted {{$a}} late",
|
||||
"syncblockedusercomponent": "user grade",
|
||||
"timemodified": "Last modified",
|
||||
"timeremaining": "Time remaining",
|
||||
"ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.",
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co
|
|||
import { IonicPage, NavController, NavParams } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper';
|
||||
|
@ -125,11 +125,20 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
|
|||
}).then(() => {
|
||||
|
||||
// Get submission status. Ignore cache to get the latest data.
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind, false, true)
|
||||
.catch((err) => {
|
||||
const options = {
|
||||
userId: this.userId,
|
||||
isBlind: this.isBlind,
|
||||
cmId: this.assign.cmid,
|
||||
filter: false,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
};
|
||||
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, options).catch((err) => {
|
||||
// Cannot connect. Get cached data.
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind)
|
||||
.then((response) => {
|
||||
options.filter = true;
|
||||
options.readingStrategy = CoreSitesReadingStrategy.PreferCache;
|
||||
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, options).then((response) => {
|
||||
const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt);
|
||||
|
||||
// Check if the user can edit it in offline.
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreEventsProvider, CoreEventObserver } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
|
||||
|
@ -23,6 +23,7 @@ import {
|
|||
AddonModAssignProvider, AddonModAssignAssign, AddonModAssignGrade, AddonModAssignSubmission
|
||||
} from '../../providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
|
||||
import { AddonModAssignSyncProvider, AddonModAssignSync } from '../../providers/assign-sync';
|
||||
import { AddonModAssignHelperProvider, AddonModAssignSubmissionFormatted } from '../../providers/helper';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
|
||||
|
@ -54,10 +55,11 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
|||
protected moduleId: number; // Module ID the submission belongs to.
|
||||
protected courseId: number; // Course ID the assignment belongs to.
|
||||
protected selectedStatus: string; // The status to see.
|
||||
protected gradedObserver; // Observer to refresh data when a grade changes.
|
||||
protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes.
|
||||
protected syncObserver: CoreEventObserver; // OObserver to refresh data when the async is synchronized.
|
||||
protected submissionsData: {canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]};
|
||||
|
||||
constructor(navParams: NavParams, protected sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
|
||||
constructor(navParams: NavParams, protected sitesProvider: CoreSitesProvider, protected eventsProvider: CoreEventsProvider,
|
||||
protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService,
|
||||
protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
|
||||
protected assignHelper: AddonModAssignHelperProvider, protected groupsProvider: CoreGroupsProvider) {
|
||||
|
@ -79,22 +81,37 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
|||
|
||||
// Update data if some grade changes.
|
||||
this.gradedObserver = eventsProvider.on(AddonModAssignProvider.GRADED_EVENT, (data) => {
|
||||
if (this.assign && data.assignmentId == this.assign.id && data.userId == sitesProvider.getCurrentSiteUserId()) {
|
||||
if (this.loaded && this.assign && data.assignmentId == this.assign.id &&
|
||||
data.userId == sitesProvider.getCurrentSiteUserId()) {
|
||||
// Grade changed, refresh the data.
|
||||
this.loaded = false;
|
||||
|
||||
this.refreshAllData().finally(() => {
|
||||
this.refreshAllData(true).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
}, sitesProvider.getCurrentSiteId());
|
||||
|
||||
// Refresh data if this assign is synchronized.
|
||||
const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED];
|
||||
this.syncObserver = eventsProvider.onMultiple(events, (data) => {
|
||||
if (!this.loaded || data.context == 'submission-list') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
|
||||
this.refreshAllData(false).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}, this.sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.fetchAssignment().finally(() => {
|
||||
this.fetchAssignment(true).finally(() => {
|
||||
if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) {
|
||||
// Take first and load it.
|
||||
this.loadSubmission(this.submissions[0]);
|
||||
|
@ -107,34 +124,49 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
|||
/**
|
||||
* Fetch assignment data.
|
||||
*
|
||||
* @param sync Whether to try to synchronize data.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected fetchAssignment(): Promise<any> {
|
||||
protected async fetchAssignment(sync?: boolean): Promise<void> {
|
||||
try {
|
||||
// Get assignment data.
|
||||
this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId);
|
||||
|
||||
// Get assignment data.
|
||||
return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => {
|
||||
this.title = assign.name || this.title;
|
||||
this.assign = assign;
|
||||
this.title = this.assign.name || this.title;
|
||||
|
||||
// Get assignment submissions.
|
||||
return this.assignProvider.getSubmissions(assign.id);
|
||||
}).then((data) => {
|
||||
if (!data.canviewsubmissions) {
|
||||
// User shouldn't be able to reach here.
|
||||
return Promise.reject(null);
|
||||
if (sync) {
|
||||
try {
|
||||
// Try to synchronize data.
|
||||
const result = await AddonModAssignSync.instance.syncAssign(this.assign.id);
|
||||
|
||||
if (result && result.updated) {
|
||||
this.eventsProvider.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, {
|
||||
assignId: this.assign.id,
|
||||
warnings: result.warnings,
|
||||
gradesBlocked: result.gradesBlocked,
|
||||
context: 'submission-list',
|
||||
}, this.sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors, probably user is offline or sync is blocked.
|
||||
}
|
||||
}
|
||||
|
||||
this.submissionsData = data;
|
||||
// Get assignment submissions.
|
||||
this.submissionsData = await this.assignProvider.getSubmissions(this.assign.id, {cmId: this.assign.cmid});
|
||||
|
||||
if (!this.submissionsData.canviewsubmissions) {
|
||||
// User shouldn't be able to reach here.
|
||||
throw new Error('Cannot view submissions.');
|
||||
}
|
||||
|
||||
// Check if groupmode is enabled to avoid showing wrong numbers.
|
||||
return this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false).then((groupInfo) => {
|
||||
this.groupInfo = groupInfo;
|
||||
this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false);
|
||||
|
||||
return this.setGroup(this.groupsProvider.validateGroupId(this.groupId, groupInfo));
|
||||
});
|
||||
}).catch((error) => {
|
||||
await this.setGroup(this.groupsProvider.validateGroupId(this.groupId, this.groupInfo));
|
||||
} catch (error) {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,7 +192,8 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
|||
const promises = [
|
||||
this.assignHelper.getSubmissionsUserData(this.assign, this.submissionsData.submissions, this.groupId),
|
||||
// Get assignment grades only if workflow is not enabled to check grading date.
|
||||
!this.assign.markingworkflow ? this.assignProvider.getAssignmentGrades(this.assign.id) : Promise.resolve(null),
|
||||
!this.assign.markingworkflow ? this.assignProvider.getAssignmentGrades(this.assign.id, {cmId: this.assign.cmid}) :
|
||||
Promise.resolve(null),
|
||||
];
|
||||
|
||||
return Promise.all(promises).then(([submissions, grades]: [AddonModAssignSubmissionFormatted[], AddonModAssignGrade[]]) => {
|
||||
|
@ -265,9 +298,10 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
|||
/**
|
||||
* Refresh all the data.
|
||||
*
|
||||
* @param sync Whether to try to synchronize data.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected refreshAllData(): Promise<any> {
|
||||
protected refreshAllData(sync?: boolean): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.assignProvider.invalidateAssignmentData(this.courseId));
|
||||
|
@ -279,7 +313,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
return Promise.all(promises).finally(() => {
|
||||
return this.fetchAssignment();
|
||||
return this.fetchAssignment(sync);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -289,7 +323,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
|||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshList(refresher: any): void {
|
||||
this.refreshAllData().finally(() => {
|
||||
this.refreshAllData(true).finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
}
|
||||
|
@ -299,6 +333,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
|||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.gradedObserver && this.gradedObserver.off();
|
||||
this.syncObserver && this.syncObserver.off();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -137,7 +137,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit {
|
|||
}
|
||||
|
||||
return Promise.all(promises).finally(() => {
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh();
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true);
|
||||
|
||||
return this.fetchSubmission();
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
|
@ -25,12 +25,14 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
|
|||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
|
||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||
import { AddonModAssignProvider, AddonModAssignAssign } from './assign';
|
||||
import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync';
|
||||
import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission } from './assign';
|
||||
import { AddonModAssignOfflineProvider } from './assign-offline';
|
||||
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
|
||||
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
|
||||
|
||||
import { makeSingleton } from '@singletons/core.singletons';
|
||||
|
||||
/**
|
||||
* Data returned by an assign sync.
|
||||
*/
|
||||
|
@ -44,6 +46,11 @@ export interface AddonModAssignSyncResult {
|
|||
* Whether data was updated in the site.
|
||||
*/
|
||||
updated: boolean;
|
||||
|
||||
/**
|
||||
* Whether some grade couldn't be synced because it was blocked.
|
||||
*/
|
||||
gradesBlocked: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,6 +60,7 @@ export interface AddonModAssignSyncResult {
|
|||
export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
||||
|
||||
static AUTO_SYNCED = 'addon_mod_assign_autom_synced';
|
||||
static MANUAL_SYNCED = 'addon_mod_assign_manual_synced';
|
||||
|
||||
protected componentTranslate: string;
|
||||
|
||||
|
@ -79,6 +87,17 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
|||
this.componentTranslate = courseProvider.translateModuleName('assign');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sync ID for a certain user grade.
|
||||
*
|
||||
* @param assignId Assign ID.
|
||||
* @param userId User the grade belongs to.
|
||||
* @return Sync ID.
|
||||
*/
|
||||
getGradeSyncId(assignId: number, userId: number): string {
|
||||
return 'assignGrade#' + assignId + '#' + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get scale selected option.
|
||||
*
|
||||
|
@ -121,7 +140,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
|||
* @param force Wether to force sync not depending on last execution.
|
||||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
syncAllAssignments(siteId?: string, force?: boolean): Promise<any> {
|
||||
syncAllAssignments(siteId?: string, force?: boolean): Promise<void> {
|
||||
return this.syncOnSites('all assignments', this.syncAllAssignmentsFunc.bind(this), [force], siteId);
|
||||
}
|
||||
|
||||
|
@ -132,26 +151,25 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
|||
* @param force Wether to force sync not depending on last execution.
|
||||
* @param Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected syncAllAssignmentsFunc(siteId?: string, force?: boolean): Promise<any> {
|
||||
protected async syncAllAssignmentsFunc(siteId?: string, force?: boolean): Promise<void> {
|
||||
// Get all assignments that have offline data.
|
||||
return this.assignOfflineProvider.getAllAssigns(siteId).then((assignIds) => {
|
||||
// Sync all assignments that haven't been synced for a while.
|
||||
const promises = assignIds.map((assignId) => {
|
||||
const promise = force ? this.syncAssign(assignId, siteId) : this.syncAssignIfNeeded(assignId, siteId);
|
||||
const assignIds = await this.assignOfflineProvider.getAllAssigns(siteId);
|
||||
|
||||
return promise.then((data) => {
|
||||
if (data && data.updated) {
|
||||
// Sync done. Send event.
|
||||
this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, {
|
||||
assignId: assignId,
|
||||
warnings: data.warnings
|
||||
}, siteId);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Try to sync all assignments.
|
||||
await Promise.all(assignIds.map(async (assignId) => {
|
||||
const data = force ? await this.syncAssign(assignId, siteId) : await this.syncAssignIfNeeded(assignId, siteId);
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
if (!data || !data.updated) {
|
||||
// Not updated.
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, {
|
||||
assignId: assignId,
|
||||
warnings: data.warnings,
|
||||
gradesBlocked: data.gradesBlocked,
|
||||
}, siteId);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,12 +179,12 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the assign is synced or it doesn't need to be synced.
|
||||
*/
|
||||
syncAssignIfNeeded(assignId: number, siteId?: string): Promise<void | AddonModAssignSyncResult> {
|
||||
return this.isSyncNeeded(assignId, siteId).then((needed) => {
|
||||
if (needed) {
|
||||
return this.syncAssign(assignId, siteId);
|
||||
}
|
||||
});
|
||||
async syncAssignIfNeeded(assignId: number, siteId?: string): Promise<void | AddonModAssignSyncResult> {
|
||||
const needed = await this.isSyncNeeded(assignId, siteId);
|
||||
|
||||
if (needed) {
|
||||
return this.syncAssign(assignId, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,18 +194,9 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved in success.
|
||||
*/
|
||||
syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
|
||||
async syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const promises: Promise<any>[] = [],
|
||||
result: AddonModAssignSyncResult = {
|
||||
warnings: [],
|
||||
updated: false
|
||||
};
|
||||
let assign: AddonModAssignAssign,
|
||||
courseId: number,
|
||||
syncPromise: Promise<any>;
|
||||
|
||||
if (this.isSyncing(assignId, siteId)) {
|
||||
// There's already a sync ongoing for this assign, return the promise.
|
||||
return this.getOngoingSync(assignId, siteId);
|
||||
|
@ -195,79 +204,126 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
|||
|
||||
// Verify that assign isn't blocked.
|
||||
if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) {
|
||||
this.logger.debug('Cannot sync assign ' + assignId + ' because it is blocked.');
|
||||
this.logger.error('Cannot sync assign ' + assignId + ' because it is blocked.');
|
||||
|
||||
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
|
||||
throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
|
||||
}
|
||||
|
||||
return this.addOngoingSync(assignId, this.performSyncAssign(assignId, siteId), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the assign submission.
|
||||
*
|
||||
* @param assignId Assign ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved in success.
|
||||
*/
|
||||
protected async performSyncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
|
||||
|
||||
this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId);
|
||||
|
||||
// Get offline submissions to be sent.
|
||||
promises.push(this.assignOfflineProvider.getAssignSubmissions(assignId, siteId).catch(() => {
|
||||
// No offline data found, return empty array.
|
||||
return [];
|
||||
}));
|
||||
const result: AddonModAssignSyncResult = {
|
||||
warnings: [],
|
||||
updated: false,
|
||||
gradesBlocked: [],
|
||||
};
|
||||
|
||||
// Get offline submission grades to be sent.
|
||||
promises.push(this.assignOfflineProvider.getAssignSubmissionsGrade(assignId, siteId).catch(() => {
|
||||
// No offline data found, return empty array.
|
||||
return [];
|
||||
}));
|
||||
// Load offline data and sync offline logs.
|
||||
const promisesResults = await Promise.all([
|
||||
this.getOfflineSubmissions(assignId, siteId),
|
||||
this.getOfflineGrades(assignId, siteId),
|
||||
this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId),
|
||||
]);
|
||||
|
||||
// Sync offline logs.
|
||||
promises.push(this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId));
|
||||
const submissions = promisesResults[0];
|
||||
const grades = promisesResults[1];
|
||||
|
||||
syncPromise = Promise.all(promises).then((results) => {
|
||||
const submissions = results[0],
|
||||
grades = results[1];
|
||||
if (!submissions.length && !grades.length) {
|
||||
// Nothing to sync.
|
||||
await this.utils.ignoreErrors(this.setSyncTime(assignId, siteId));
|
||||
|
||||
if (!submissions.length && !grades.length) {
|
||||
// Nothing to sync.
|
||||
return;
|
||||
} else if (!this.appProvider.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid;
|
||||
|
||||
return this.assignProvider.getAssignmentById(courseId, assignId, false, siteId).then((assignData) => {
|
||||
assign = assignData;
|
||||
|
||||
const promises = [];
|
||||
|
||||
submissions.forEach((submission) => {
|
||||
promises.push(this.syncSubmission(assign, submission, result.warnings, siteId).then(() => {
|
||||
result.updated = true;
|
||||
}));
|
||||
});
|
||||
|
||||
grades.forEach((grade) => {
|
||||
promises.push(this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId).then(() => {
|
||||
result.updated = true;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
if (result.updated) {
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
return this.assignProvider.invalidateContent(assign.cmid, courseId, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
// Sync finished, set sync time.
|
||||
return this.setSyncTime(assignId, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}).then(() => {
|
||||
// All done, return the result.
|
||||
return result;
|
||||
});
|
||||
} else if (!this.appProvider.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
throw new Error(this.translate.instant('core.cannotconnect'));
|
||||
}
|
||||
|
||||
return this.addOngoingSync(assignId, syncPromise, siteId);
|
||||
const courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid;
|
||||
|
||||
const assign = await this.assignProvider.getAssignmentById(courseId, assignId, {siteId});
|
||||
|
||||
let promises = [];
|
||||
|
||||
promises = promises.concat(submissions.map(async (submission) => {
|
||||
await this.syncSubmission(assign, submission, result.warnings, siteId);
|
||||
|
||||
result.updated = true;
|
||||
}));
|
||||
|
||||
promises = promises.concat(grades.map(async (grade) => {
|
||||
try {
|
||||
await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId);
|
||||
|
||||
result.updated = true;
|
||||
} catch (error) {
|
||||
if (error instanceof CoreSyncBlockedError) {
|
||||
// Grade blocked, but allow finish the sync.
|
||||
result.gradesBlocked.push(grade.userid);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (result.updated) {
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
await this.utils.ignoreErrors(this.assignProvider.invalidateContent(assign.cmid, courseId, siteId));
|
||||
}
|
||||
|
||||
// Sync finished, set sync time.
|
||||
await this.utils.ignoreErrors(this.setSyncTime(assignId, siteId));
|
||||
|
||||
// All done, return the result.
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offline grades to be sent.
|
||||
*
|
||||
* @param assignId Assign ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise with grades.
|
||||
*/
|
||||
protected async getOfflineGrades(assignId: number, siteId: string): Promise<any[]> {
|
||||
try {
|
||||
const submissions = await this.assignOfflineProvider.getAssignSubmissionsGrade(assignId, siteId);
|
||||
|
||||
return submissions;
|
||||
} catch (error) {
|
||||
// No offline data found, return empty array.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offline submissions to be sent.
|
||||
*
|
||||
* @param assignId Assign ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise with submissions.
|
||||
*/
|
||||
protected async getOfflineSubmissions(assignId: number, siteId: string): Promise<any[]> {
|
||||
try {
|
||||
const submissions = await this.assignOfflineProvider.getAssignSubmissions(assignId, siteId);
|
||||
|
||||
return submissions;
|
||||
} catch (error) {
|
||||
// No offline data found, return empty array.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,83 +335,82 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string): Promise<any> {
|
||||
const userId = offlineData.userid,
|
||||
pluginData = {};
|
||||
let discardError,
|
||||
submission;
|
||||
protected async syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string)
|
||||
: Promise<void> {
|
||||
|
||||
return this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId).then((status) => {
|
||||
const promises = [];
|
||||
const userId = offlineData.userid;
|
||||
const pluginData = {};
|
||||
const options = {
|
||||
userId,
|
||||
cmId: assign.cmid,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt);
|
||||
const status = await this.assignProvider.getSubmissionStatus(assign.id, options);
|
||||
|
||||
if (submission.timemodified != offlineData.onlinetimemodified) {
|
||||
// The submission was modified in Moodle, discard the submission.
|
||||
discardError = this.translate.instant('addon.mod_assign.warningsubmissionmodified');
|
||||
const submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt);
|
||||
|
||||
return;
|
||||
if (submission.timemodified != offlineData.onlinetimemodified) {
|
||||
// The submission was modified in Moodle, discard the submission.
|
||||
this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name,
|
||||
this.translate.instant('addon.mod_assign.warningsubmissionmodified'));
|
||||
|
||||
return this.deleteSubmissionData(assign, submission, offlineData, siteId);
|
||||
}
|
||||
|
||||
try {
|
||||
// Prepare plugins data.
|
||||
await Promise.all(submission.plugins.map(async (plugin) => {
|
||||
await this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData, siteId);
|
||||
}));
|
||||
|
||||
// Now save the submission.
|
||||
if (Object.keys(pluginData).length > 0) {
|
||||
await this.assignProvider.saveSubmissionOnline(assign.id, pluginData, siteId);
|
||||
}
|
||||
|
||||
submission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData,
|
||||
siteId));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
// Now save the submission.
|
||||
let promise;
|
||||
|
||||
if (!Object.keys(pluginData).length) {
|
||||
// Nothing to save.
|
||||
promise = Promise.resolve();
|
||||
} else {
|
||||
promise = this.assignProvider.saveSubmissionOnline(assign.id, pluginData, siteId);
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
if (assign.submissiondrafts && offlineData.submitted) {
|
||||
// The user submitted the assign manually. Submit it for grading.
|
||||
return this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionstatement, siteId);
|
||||
}
|
||||
}).then(() => {
|
||||
// Submission data sent, update cached data. No need to block the user for this.
|
||||
this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId);
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (error && this.utils.isWebServiceError(error)) {
|
||||
// A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
|
||||
discardError = this.textUtils.getErrorMessageFromError(error);
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
// Delete the offline data.
|
||||
return this.assignOfflineProvider.deleteSubmission(assign.id, userId, siteId).then(() => {
|
||||
const promises = [];
|
||||
|
||||
submission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.deletePluginOfflineData(assign, submission, plugin, offlineData, siteId));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
}).then(() => {
|
||||
if (discardError) {
|
||||
// Submission was discarded, add a warning.
|
||||
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: assign.name,
|
||||
error: discardError
|
||||
});
|
||||
|
||||
if (warnings.indexOf(message) == -1) {
|
||||
warnings.push(message);
|
||||
}
|
||||
if (assign.submissiondrafts && offlineData.submitted) {
|
||||
// The user submitted the assign manually. Submit it for grading.
|
||||
await this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionstatement, siteId);
|
||||
}
|
||||
});
|
||||
|
||||
// Submission data sent, update cached data. No need to block the user for this.
|
||||
this.assignProvider.getSubmissionStatus(assign.id, options);
|
||||
} catch (error) {
|
||||
if (!error || !this.utils.isWebServiceError(error)) {
|
||||
// Local error, reject.
|
||||
throw error;
|
||||
}
|
||||
|
||||
// A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
|
||||
this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name,
|
||||
this.textUtils.getErrorMessageFromError(error));
|
||||
}
|
||||
|
||||
// Delete the offline data.
|
||||
await this.deleteSubmissionData(assign, submission, offlineData, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the submission offline data (not grades).
|
||||
*
|
||||
* @param assign Assign.
|
||||
* @param submission Submission.
|
||||
* @param offlineData Offline data.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async deleteSubmissionData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, offlineData: any,
|
||||
siteId?: string): Promise<void> {
|
||||
|
||||
// Delete the offline data.
|
||||
await this.assignOfflineProvider.deleteSubmission(assign.id, offlineData.userid, siteId);
|
||||
|
||||
// Delete plugins data.
|
||||
await Promise.all(submission.plugins.map(async (plugin) => {
|
||||
await this.submissionDelegate.deletePluginOfflineData(assign, submission, plugin, offlineData, siteId);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -368,90 +423,95 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected syncSubmissionGrade(assign: AddonModAssignAssign, offlineData: any, warnings: string[], courseId: number,
|
||||
protected async syncSubmissionGrade(assign: AddonModAssignAssign, offlineData: any, warnings: string[], courseId: number,
|
||||
siteId?: string): Promise<any> {
|
||||
|
||||
const userId = offlineData.userid;
|
||||
let discardError;
|
||||
const syncId = this.getGradeSyncId(assign.id, userId);
|
||||
const options = {
|
||||
userId,
|
||||
cmId: assign.cmid,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
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);
|
||||
// Check if this grade sync is blocked.
|
||||
if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, syncId, siteId)) {
|
||||
this.logger.error(`Cannot sync grade for assign ${assign.id} and user ${userId} because it is blocked.!!!!`);
|
||||
|
||||
if (timemodified > offlineData.timemodified) {
|
||||
// The submission grade was modified in Moodle, discard it.
|
||||
discardError = this.translate.instant('addon.mod_assign.warningsubmissiongrademodified');
|
||||
throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked',
|
||||
{$a: this.translate.instant('addon.mod_assign.syncblockedusercomponent')}));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
const status = await this.assignProvider.getSubmissionStatus(assign.id, options);
|
||||
|
||||
// If grade has been modified from gradebook, do not use offline.
|
||||
return this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true).then((grades) => {
|
||||
return this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId).then((gradeInfo) => {
|
||||
const timemodified = status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified);
|
||||
|
||||
// Override offline grade and outcomes based on the gradebook data.
|
||||
grades.forEach((grade) => {
|
||||
if (grade.gradedategraded >= offlineData.timemodified) {
|
||||
if (!grade.outcomeid && !grade.scaleid) {
|
||||
if (gradeInfo && gradeInfo.scale) {
|
||||
offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.gradeformatted);
|
||||
} else {
|
||||
offlineData.grade = parseFloat(grade.gradeformatted) || null;
|
||||
}
|
||||
} else if (grade.outcomeid && this.assignProvider.isOutcomesEditEnabled() && gradeInfo.outcomes) {
|
||||
gradeInfo.outcomes.forEach((outcome, index) => {
|
||||
if (outcome.scale && grade.itemnumber == index) {
|
||||
offlineData.outcomes[grade.itemnumber] = this.getSelectedScaleId(outcome.scale,
|
||||
outcome.selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (timemodified > offlineData.timemodified) {
|
||||
// The submission grade was modified in Moodle, discard it.
|
||||
this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name,
|
||||
this.translate.instant('addon.mod_assign.warningsubmissiongrademodified'));
|
||||
|
||||
return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId);
|
||||
}
|
||||
|
||||
// If grade has been modified from gradebook, do not use offline.
|
||||
const grades = await this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true);
|
||||
|
||||
const gradeInfo = await this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId);
|
||||
|
||||
// Override offline grade and outcomes based on the gradebook data.
|
||||
grades.forEach((grade) => {
|
||||
if (grade.gradedategraded >= offlineData.timemodified) {
|
||||
if (!grade.outcomeid && !grade.scaleid) {
|
||||
if (gradeInfo && gradeInfo.scale) {
|
||||
offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.gradeformatted);
|
||||
} else {
|
||||
offlineData.grade = parseFloat(grade.gradeformatted) || null;
|
||||
}
|
||||
} else if (grade.outcomeid && this.assignProvider.isOutcomesEditEnabled() && gradeInfo.outcomes) {
|
||||
gradeInfo.outcomes.forEach((outcome, index) => {
|
||||
if (outcome.scale && grade.itemnumber == index) {
|
||||
offlineData.outcomes[grade.itemnumber] = this.getSelectedScaleId(outcome.scale,
|
||||
outcome.selected);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).then(() => {
|
||||
// Now submit the grade.
|
||||
return this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptnumber,
|
||||
offlineData.addattempt, offlineData.workflowstate, offlineData.applytoall, offlineData.outcomes,
|
||||
offlineData.plugindata, siteId).then(() => {
|
||||
// Grades sent.
|
||||
// Discard grades drafts.
|
||||
const promises = [];
|
||||
if (status.feedback && status.feedback.plugins) {
|
||||
status.feedback.plugins.forEach((plugin) => {
|
||||
promises.push(this.feedbackDelegate.discardPluginFeedbackData(assign.id, userId, plugin, siteId));
|
||||
});
|
||||
}
|
||||
|
||||
// Update cached data.
|
||||
promises.push(this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId));
|
||||
|
||||
return Promise.all(promises);
|
||||
}).catch((error) => {
|
||||
if (error && this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be submitted. Discard the offline data.
|
||||
discardError = this.textUtils.getErrorMessageFromError(error);
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).then(() => {
|
||||
// Delete the offline data.
|
||||
return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId);
|
||||
}).then(() => {
|
||||
if (discardError) {
|
||||
// Submission grade was discarded, add a warning.
|
||||
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: assign.name,
|
||||
error: discardError
|
||||
});
|
||||
|
||||
if (warnings.indexOf(message) == -1) {
|
||||
warnings.push(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Now submit the grade.
|
||||
await this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptnumber,
|
||||
offlineData.addattempt, offlineData.workflowstate, offlineData.applytoall, offlineData.outcomes,
|
||||
offlineData.plugindata, siteId);
|
||||
|
||||
// Grades sent. Discard grades drafts.
|
||||
const promises = [];
|
||||
if (status.feedback && status.feedback.plugins) {
|
||||
status.feedback.plugins.forEach((plugin) => {
|
||||
promises.push(this.feedbackDelegate.discardPluginFeedbackData(assign.id, userId, plugin, siteId));
|
||||
});
|
||||
}
|
||||
|
||||
// Update cached data.
|
||||
promises.push(this.assignProvider.getSubmissionStatus(assign.id, options));
|
||||
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (!error || !this.utils.isWebServiceError(error)) {
|
||||
// Local error, reject.
|
||||
throw error;
|
||||
}
|
||||
|
||||
// A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
|
||||
this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name,
|
||||
this.textUtils.getErrorMessageFromError(error));
|
||||
}
|
||||
|
||||
// Delete the offline data.
|
||||
await this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
export class AddonModAssignSync extends makeSingleton(AddonModAssignSyncProvider) {}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
|
@ -25,9 +25,10 @@ import { CoreGradesProvider } from '@core/grades/providers/grades';
|
|||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
|
||||
import { AddonModAssignOfflineProvider } from './assign-offline';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreInterceptor } from '@classes/interceptor';
|
||||
import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
|
||||
import { CoreCourseCommonModWSOptions } from '@core/course/providers/course';
|
||||
|
||||
/**
|
||||
* Service that provides some functions for assign.
|
||||
|
@ -143,12 +144,11 @@ export class AddonModAssignProvider {
|
|||
*
|
||||
* @param courseId Course ID the assignment belongs to.
|
||||
* @param cmId Assignment module ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the assignment.
|
||||
*/
|
||||
getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignAssign> {
|
||||
return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId);
|
||||
getAssignment(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModAssignAssign> {
|
||||
return this.getAssignmentByField(courseId, 'cmid', cmId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -157,27 +157,23 @@ export class AddonModAssignProvider {
|
|||
* @param courseId Course ID.
|
||||
* @param key Name of the property to check.
|
||||
* @param value Value to search.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the assignment is retrieved.
|
||||
*/
|
||||
protected getAssignmentByField(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string)
|
||||
protected getAssignmentByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {})
|
||||
: Promise<AddonModAssignAssign> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId],
|
||||
includenotenrolledcourses: 1
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getAssignmentCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
courseids: [courseId],
|
||||
includenotenrolledcourses: 1,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getAssignmentCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModAssignProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
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.
|
||||
|
@ -206,13 +202,12 @@ export class AddonModAssignProvider {
|
|||
* Get an assignment by instance ID.
|
||||
*
|
||||
* @param courseId Course ID the assignment belongs to.
|
||||
* @param cmId Assignment instance ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param id Assignment instance ID.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the assignment.
|
||||
*/
|
||||
getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignAssign> {
|
||||
return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId);
|
||||
getAssignmentById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModAssignAssign> {
|
||||
return this.getAssignmentByField(courseId, 'id', id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -230,24 +225,22 @@ export class AddonModAssignProvider {
|
|||
*
|
||||
* @param assignId Assignment Id.
|
||||
* @param userId User Id to be blinded.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the user blind id.
|
||||
*/
|
||||
getAssignmentUserMappings(assignId: number, userId: number, ignoreCache?: boolean, siteId?: string): Promise<number> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
assignmentids: [assignId]
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getAssignmentUserMappingsCacheKey(assignId),
|
||||
updateFrequency: CoreSite.FREQUENCY_OFTEN
|
||||
};
|
||||
getAssignmentUserMappings(assignId: number, userId: number, options: CoreCourseCommonModWSOptions = {}): Promise<number> {
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
assignmentids: [assignId],
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getAssignmentUserMappingsCacheKey(assignId),
|
||||
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
||||
component: AddonModAssignProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_assign_get_user_mappings', params, preSets)
|
||||
.then((response: AddonModAssignGetUserMappingsResult): any => {
|
||||
|
@ -293,23 +286,21 @@ export class AddonModAssignProvider {
|
|||
* Returns grade information from assign_grades for the requested assignment id
|
||||
*
|
||||
* @param assignId Assignment Id.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Resolved with requested info when done.
|
||||
*/
|
||||
getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignGrade[]> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
assignmentids: [assignId]
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getAssignmentGradesCacheKey(assignId)
|
||||
};
|
||||
getAssignmentGrades(assignId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModAssignGrade[]> {
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
assignmentids: [assignId],
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getAssignmentGradesCacheKey(assignId),
|
||||
component: AddonModAssignProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_assign_get_grades', params, preSets).then((response: AddonModAssignGetGradesResult): any => {
|
||||
// Search the assignment.
|
||||
|
@ -455,26 +446,23 @@ export class AddonModAssignProvider {
|
|||
* Get an assignment submissions.
|
||||
*
|
||||
* @param assignId Assignment id.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
getSubmissions(assignId: number, ignoreCache?: boolean, siteId?: string)
|
||||
getSubmissions(assignId: number, options: CoreCourseCommonModWSOptions = {})
|
||||
: Promise<{canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
assignmentids: [assignId]
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getSubmissionsCacheKey(assignId),
|
||||
updateFrequency: CoreSite.FREQUENCY_OFTEN
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
assignmentids: [assignId],
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getSubmissionsCacheKey(assignId),
|
||||
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
||||
component: AddonModAssignProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_assign_get_submissions', params, preSets)
|
||||
.then((response: AddonModAssignGetSubmissionsResult): any => {
|
||||
|
@ -510,46 +498,40 @@ export class AddonModAssignProvider {
|
|||
* Get information about an assignment submission status for a given user.
|
||||
*
|
||||
* @param assignId Assignment instance id.
|
||||
* @param userId User Id (empty for current user).
|
||||
* @param groupId Group Id (empty for all participants).
|
||||
* @param isBlind If blind marking is enabled or not.
|
||||
* @param filter True to filter WS response and rewrite URLs, false otherwise.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site id (empty for current site).
|
||||
* @param options Other options.
|
||||
* @return Promise always resolved with the user submission status.
|
||||
*/
|
||||
getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true,
|
||||
ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignGetSubmissionStatusResult> {
|
||||
getSubmissionStatus(assignId: number, options: AddonModAssignSubmissionStatusOptions = {})
|
||||
: Promise<AddonModAssignGetSubmissionStatusResult> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const fixedParams = this.fixSubmissionStatusParams(site, userId, groupId, isBlind);
|
||||
if (options.filter === undefined || options.filter === null) {
|
||||
options.filter = true;
|
||||
}
|
||||
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const fixedParams = this.fixSubmissionStatusParams(site, options.userId, options.groupId, options.isBlind);
|
||||
|
||||
const params = {
|
||||
assignid: assignId,
|
||||
userid: fixedParams.userId
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId,
|
||||
fixedParams.isBlind),
|
||||
getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account.
|
||||
filter: filter,
|
||||
rewriteurls: filter
|
||||
};
|
||||
|
||||
assignid: assignId,
|
||||
userid: fixedParams.userId,
|
||||
};
|
||||
if (fixedParams.groupId) {
|
||||
params['groupid'] = fixedParams.groupId;
|
||||
}
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
if (!filter) {
|
||||
const preSets = {
|
||||
cacheKey: this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId,
|
||||
fixedParams.isBlind),
|
||||
getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account.
|
||||
filter: options.filter,
|
||||
rewriteurls: options.filter,
|
||||
component: AddonModAssignProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
// Don't cache when getting text without filters.
|
||||
// @todo Change this to support offline editing.
|
||||
preSets.saveToCache = false;
|
||||
}
|
||||
saveToCache: options.filter,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_assign_get_submission_status', params, preSets);
|
||||
});
|
||||
|
@ -560,23 +542,24 @@ export class AddonModAssignProvider {
|
|||
* If the data doesn't include the user submission, retry ignoring cache.
|
||||
*
|
||||
* @param assign Assignment.
|
||||
* @param userId User id (empty for current user).
|
||||
* @param groupId Group Id (empty for all participants).
|
||||
* @param isBlind If blind marking is enabled or not.
|
||||
* @param filter True to filter WS response and rewrite URLs, false otherwise.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site id (empty for current site).
|
||||
* @param options Other options.
|
||||
* @return 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<AddonModAssignGetSubmissionStatusResult> {
|
||||
getSubmissionStatusWithRetry(assign: any, options: AddonModAssignSubmissionStatusOptions = {})
|
||||
: Promise<AddonModAssignGetSubmissionStatusResult> {
|
||||
options.cmId = options.cmId || assign.cmid;
|
||||
|
||||
return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, ignoreCache, siteId).then((response) => {
|
||||
return this.getSubmissionStatus(assign.id, options).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(() => {
|
||||
const newOptions = {
|
||||
...options, // Include all the original options.
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
};
|
||||
|
||||
return this.getSubmissionStatus(assign.id, newOptions).catch(() => {
|
||||
// Error, return the first result even if it doesn't have the user submission.
|
||||
return response;
|
||||
});
|
||||
|
@ -650,35 +633,32 @@ export class AddonModAssignProvider {
|
|||
*
|
||||
* @param assignId Assignment id.
|
||||
* @param groupId Group id. If not defined, 0.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the list of participants and summary of submissions.
|
||||
*/
|
||||
listParticipants(assignId: number, groupId?: number, ignoreCache?: boolean, siteId?: string)
|
||||
listParticipants(assignId: number, groupId?: number, options: CoreCourseCommonModWSOptions = {})
|
||||
: Promise<AddonModAssignParticipant[]> {
|
||||
|
||||
groupId = groupId || 0;
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
if (!site.wsAvailable('mod_assign_list_participants')) {
|
||||
// Silently fail if is not available. (needs Moodle version >= 3.2)
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
const params = {
|
||||
assignid: assignId,
|
||||
groupid: groupId,
|
||||
filter: ''
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.listParticipantsCacheKey(assignId, groupId),
|
||||
updateFrequency: CoreSite.FREQUENCY_OFTEN
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
assignid: assignId,
|
||||
groupid: groupId,
|
||||
filter: '',
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.listParticipantsCacheKey(assignId, groupId),
|
||||
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
||||
component: AddonModAssignProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_assign_list_participants', params, preSets);
|
||||
});
|
||||
|
@ -769,7 +749,7 @@ export class AddonModAssignProvider {
|
|||
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.getAssignment(courseId, moduleId, false, siteId).then((assign) => {
|
||||
return this.getAssignment(courseId, moduleId, {siteId}).then((assign) => {
|
||||
const promises = [];
|
||||
|
||||
// Do not invalidate assignment data before getting assignment info, we need it!
|
||||
|
@ -1014,7 +994,10 @@ export class AddonModAssignProvider {
|
|||
}
|
||||
|
||||
// We need more data to decide that.
|
||||
return this.getSubmissionStatus(assignId, submission.submitid, undefined, submission.blindid).then((response) => {
|
||||
return this.getSubmissionStatus(assignId, {
|
||||
userId: submission.submitid,
|
||||
isBlind: !!submission.blindid,
|
||||
}).then((response) => {
|
||||
if (!response.feedback || !response.feedback.gradeddate) {
|
||||
// Not graded.
|
||||
return true;
|
||||
|
@ -1304,6 +1287,16 @@ export class AddonModAssignProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to pass to get submission status.
|
||||
*/
|
||||
export type AddonModAssignSubmissionStatusOptions = CoreCourseCommonModWSOptions & {
|
||||
userId?: number; // User Id (empty for current user).
|
||||
groupId?: number; // Group Id (empty for all participants).
|
||||
isBlind?: boolean; // If blind marking is enabled or not.
|
||||
filter?: boolean; // True to filter WS response and rewrite URLs, false otherwise. Defaults to true.
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign data returned by mod_assign_get_assignments.
|
||||
*/
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreFileProvider } from '@providers/file';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
|
||||
|
@ -209,29 +209,29 @@ export class AddonModAssignHelperProvider {
|
|||
*
|
||||
* @param assign Assignment object.
|
||||
* @param groupId Group Id.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the list of participants and summary of submissions.
|
||||
*/
|
||||
getParticipants(assign: AddonModAssignAssign, groupId?: number, ignoreCache?: boolean, siteId?: string)
|
||||
getParticipants(assign: AddonModAssignAssign, groupId?: number, options: CoreSitesCommonWSOptions = {})
|
||||
: Promise<AddonModAssignParticipant[]> {
|
||||
|
||||
groupId = groupId || 0;
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.assignProvider.listParticipants(assign.id, groupId, ignoreCache, siteId).then((participants) => {
|
||||
const modOptions = {cmId: assign.cmid, ...options}; // Create new options including all existing ones.
|
||||
|
||||
return this.assignProvider.listParticipants(assign.id, groupId, modOptions).then((participants) => {
|
||||
if (groupId || participants && participants.length > 0) {
|
||||
return participants;
|
||||
}
|
||||
|
||||
// If no participants returned and all groups specified, get participants by groups.
|
||||
return this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((info) => {
|
||||
return this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, modOptions.siteId).then((info) => {
|
||||
const promises = [],
|
||||
participants: {[id: number]: AddonModAssignParticipant} = {};
|
||||
|
||||
info.groups.forEach((userGroup) => {
|
||||
promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, ignoreCache, siteId)
|
||||
.then((parts) => {
|
||||
promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, modOptions).then((parts) => {
|
||||
// Do not get repeated users.
|
||||
parts.forEach((participant) => {
|
||||
participants[participant.id] = participant;
|
||||
|
@ -355,14 +355,15 @@ export class AddonModAssignHelperProvider {
|
|||
* @param assign Assignment object.
|
||||
* @param submissions Submissions to get the data for.
|
||||
* @param groupId Group Id.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site id (empty for current site).
|
||||
* @param options Other options.
|
||||
* @return Promise always resolved. Resolve param is the formatted submissions.
|
||||
*/
|
||||
getSubmissionsUserData(assign: AddonModAssignAssign, submissions: AddonModAssignSubmissionFormatted[], groupId?: number,
|
||||
ignoreCache?: boolean, siteId?: string): Promise<AddonModAssignSubmissionFormatted[]> {
|
||||
options: CoreSitesCommonWSOptions = {}): Promise<AddonModAssignSubmissionFormatted[]> {
|
||||
|
||||
return this.getParticipants(assign, groupId).then((parts) => {
|
||||
const modOptions = {cmId: assign.cmid, ...options}; // Create new options including all existing ones.
|
||||
|
||||
return this.getParticipants(assign, groupId, modOptions).then((parts) => {
|
||||
const blind = assign.blindmarking && !assign.revealidentities;
|
||||
const promises = [];
|
||||
const result: AddonModAssignSubmissionFormatted[] = [];
|
||||
|
@ -399,8 +400,8 @@ export class AddonModAssignHelperProvider {
|
|||
// Blind but not blinded! (Moodle < 3.1.1, 3.2).
|
||||
delete submission.userid;
|
||||
|
||||
promise = this.assignProvider.getAssignmentUserMappings(assign.id, submission.submitid, ignoreCache, siteId).
|
||||
then((blindId) => {
|
||||
promise = this.assignProvider.getAssignmentUserMappings(assign.id, submission.submitid, modOptions)
|
||||
.then((blindId) => {
|
||||
submission.blindid = blindId;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
|
@ -80,13 +80,13 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
canUseCheckUpdates(module: any, courseId: number): boolean | Promise<boolean> {
|
||||
// Teachers cannot use the WS because it doesn't check student submissions.
|
||||
return this.assignProvider.getAssignment(courseId, module.id).then((assign) => {
|
||||
return this.assignProvider.getSubmissions(assign.id).then((data) => {
|
||||
return this.assignProvider.getSubmissions(assign.id, {cmId: module.id}).then((data) => {
|
||||
if (data.canviewsubmissions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the user can view their own submission.
|
||||
return this.assignProvider.getSubmissionStatus(assign.id).then(() => {
|
||||
return this.assignProvider.getSubmissionStatus(assign.id, {cmId: module.id}).then(() => {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
@ -108,18 +108,18 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.assignProvider.getAssignment(courseId, module.id, false, siteId).then((assign) => {
|
||||
return this.assignProvider.getAssignment(courseId, module.id, {siteId}).then((assign) => {
|
||||
// Get intro files and attachments.
|
||||
let files = assign.introattachments || [];
|
||||
files = files.concat(this.getIntroFilesFromInstance(module, assign));
|
||||
|
||||
// Now get the files in the submissions.
|
||||
return this.assignProvider.getSubmissions(assign.id, false, siteId).then((data) => {
|
||||
return this.assignProvider.getSubmissions(assign.id, {cmId: module.id, siteId}).then((data) => {
|
||||
const blindMarking = assign.blindmarking && !assign.revealidentities;
|
||||
|
||||
if (data.canviewsubmissions) {
|
||||
// Teacher, get all submissions.
|
||||
return this.assignHelper.getSubmissionsUserData(assign, data.submissions, 0, false, siteId)
|
||||
return this.assignHelper.getSubmissionsUserData(assign, data.submissions, 0, {siteId})
|
||||
.then((submissions: AddonModAssignSubmissionFormatted[]) => {
|
||||
|
||||
const promises = [];
|
||||
|
@ -172,8 +172,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string)
|
||||
: Promise<any[]> {
|
||||
|
||||
return this.assignProvider.getSubmissionStatusWithRetry(assign, submitId, undefined, blindMarking, true, false, siteId)
|
||||
.then((response) => {
|
||||
return this.assignProvider.getSubmissionStatusWithRetry(assign, {
|
||||
userId: submitId,
|
||||
isBlind: blindMarking,
|
||||
siteId,
|
||||
}).then((response) => {
|
||||
const promises = [];
|
||||
let userSubmission: AddonModAssignSubmission;
|
||||
|
||||
|
@ -261,20 +264,24 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected prefetchAssign(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||
const userId = this.sitesProvider.getCurrentSiteUserId(),
|
||||
promises = [];
|
||||
const userId = this.sitesProvider.getCurrentSiteUserId();
|
||||
const promises = [];
|
||||
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const options = {
|
||||
cmId: module.id,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
// Get assignment to retrieve all its submissions.
|
||||
promises.push(this.assignProvider.getAssignment(courseId, module.id, true, siteId).then((assign) => {
|
||||
promises.push(this.assignProvider.getAssignment(courseId, module.id, options).then((assign) => {
|
||||
const subPromises = [],
|
||||
blindMarking = assign.blindmarking && !assign.revealidentities;
|
||||
|
||||
if (blindMarking) {
|
||||
subPromises.push(this.assignProvider.getAssignmentUserMappings(assign.id, undefined, true, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
subPromises.push(this.utils.ignoreErrors(this.assignProvider.getAssignmentUserMappings(assign.id, -1, options)));
|
||||
}
|
||||
|
||||
subPromises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId));
|
||||
|
@ -304,8 +311,14 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
* @return Promise resolved when prefetched, rejected otherwise.
|
||||
*/
|
||||
protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise<any> {
|
||||
const options = {
|
||||
cmId: moduleId,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
// Get submissions.
|
||||
return this.assignProvider.getSubmissions(assign.id, true, siteId).then((data) => {
|
||||
return this.assignProvider.getSubmissions(assign.id, options).then((data) => {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((groupInfo) => {
|
||||
|
@ -317,14 +330,22 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
}
|
||||
|
||||
groupInfo.groups.forEach((group) => {
|
||||
groupProms.push(this.assignHelper.getSubmissionsUserData(assign, data.submissions, group.id, true, siteId)
|
||||
groupProms.push(this.assignHelper.getSubmissionsUserData(assign, data.submissions, group.id, options)
|
||||
.then((submissions: AddonModAssignSubmissionFormatted[]) => {
|
||||
|
||||
const subPromises = [];
|
||||
|
||||
submissions.forEach((submission) => {
|
||||
subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submission.submitid,
|
||||
group.id, !!submission.blindid, true, true, siteId).then((subm) => {
|
||||
const submissionOptions = {
|
||||
userId: submission.submitid,
|
||||
groupId: group.id,
|
||||
isBlind: !!submission.blindid,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submissionOptions)
|
||||
.then((subm) => {
|
||||
return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId);
|
||||
}).catch((error) => {
|
||||
if (error && error.errorcode == 'nopermission') {
|
||||
|
@ -338,14 +359,21 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
|
||||
if (!assign.markingworkflow) {
|
||||
// Get assignment grades only if workflow is not enabled to check grading date.
|
||||
subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, true, siteId));
|
||||
subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, options));
|
||||
}
|
||||
|
||||
// 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: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) {
|
||||
subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, userId, group.id,
|
||||
false, true, true, siteId).then((subm) => {
|
||||
const submissionOptions = {
|
||||
userId,
|
||||
groupId: group.id,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submissionOptions)
|
||||
.then((subm) => {
|
||||
return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId);
|
||||
}));
|
||||
}
|
||||
|
@ -353,7 +381,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
return Promise.all(subPromises);
|
||||
}).then(() => {
|
||||
// Participiants already fetched, we don't need to ignore cache now.
|
||||
return this.assignHelper.getParticipants(assign, group.id, false, siteId).then((participants) => {
|
||||
return this.assignHelper.getParticipants(assign, group.id, {siteId}).then((participants) => {
|
||||
return this.userProvider.prefetchUserAvatars(participants, 'profileimageurl', siteId);
|
||||
}).catch(() => {
|
||||
// Fail silently (Moodle < 3.2).
|
||||
|
@ -367,8 +395,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
|
||||
// Prefetch own submission, we need to do this for teachers too so the response with error is cached.
|
||||
promises.push(
|
||||
this.assignProvider.getSubmissionStatusWithRetry(assign, userId, undefined, false, true, true, siteId)
|
||||
.then((subm) => {
|
||||
this.assignProvider.getSubmissionStatusWithRetry(assign, {
|
||||
userId,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
}).then((subm) => {
|
||||
return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId);
|
||||
}).catch((error) => {
|
||||
// Ignore if the user can't view their own submission.
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
|
||||
<core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreFileProvider } from '@providers/file';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
|
@ -73,11 +73,11 @@ export class AddonModBookProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param cmId Course module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the book is retrieved.
|
||||
*/
|
||||
getBook(courseId: number, cmId: number, siteId?: string): Promise<AddonModBookBook> {
|
||||
return this.getBookByField(courseId, 'coursemodule', cmId, siteId);
|
||||
getBook(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModBookBook> {
|
||||
return this.getBookByField(courseId, 'coursemodule', cmId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,15 +89,19 @@ export class AddonModBookProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the book is retrieved.
|
||||
*/
|
||||
protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise<AddonModBookBook> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
protected getBookByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {})
|
||||
: Promise<AddonModBookBook> {
|
||||
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getBookDataCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
};
|
||||
courseids: [courseId]
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getBookDataCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModBookProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_book_get_books_by_courses', params, preSets)
|
||||
.then((response: AddonModBookGetBooksByCoursesResult): any => {
|
||||
|
|
|
@ -127,7 +127,8 @@ export class AddonModChatChatPage {
|
|||
showChatUsers(): void {
|
||||
// Create the toc modal.
|
||||
const modal = this.modalCtrl.create('AddonModChatUsersPage', {
|
||||
sessionId: this.sessionId
|
||||
sessionId: this.sessionId,
|
||||
cmId: this.cmId,
|
||||
}, { cssClass: 'core-modal-lateral',
|
||||
showBackdrop: true,
|
||||
enableBackdropDismiss: true,
|
||||
|
@ -168,7 +169,7 @@ export class AddonModChatChatPage {
|
|||
return Promise.resolve(user.fullname);
|
||||
}
|
||||
|
||||
return this.chatProvider.getChatUsers(this.sessionId).then((data) => {
|
||||
return this.chatProvider.getChatUsers(this.sessionId, {cmId: this.cmId}).then((data) => {
|
||||
this.users = data.users;
|
||||
const user = this.users.find((user) => user.id == id);
|
||||
|
||||
|
|
|
@ -60,8 +60,8 @@ export class AddonModChatSessionMessagesPage {
|
|||
* @return 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.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId,
|
||||
{cmId: this.cmId}).then((messages) => {
|
||||
return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => {
|
||||
this.messages = <AddonModChatSessionMessageForView[]> messages;
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ export class AddonModChatSessionsPage {
|
|||
this.groupInfo = groupInfo;
|
||||
this.groupId = this.groupsProvider.validateGroupId(this.groupId, groupInfo);
|
||||
|
||||
return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll);
|
||||
return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll, {cmId: this.cmId});
|
||||
}).then((sessions: AddonModChatSessionFormatted[]) => {
|
||||
// Fetch user profiles.
|
||||
const promises = [];
|
||||
|
|
|
@ -36,6 +36,7 @@ export class AddonModChatUsersPage {
|
|||
isOnline: boolean;
|
||||
|
||||
protected sessionId: string;
|
||||
protected cmId: number;
|
||||
protected onlineObserver: any;
|
||||
|
||||
constructor(navParams: NavParams, network: Network, zone: NgZone, private appProvider: CoreAppProvider,
|
||||
|
@ -56,7 +57,7 @@ export class AddonModChatUsersPage {
|
|||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad(): void {
|
||||
this.chatProvider.getChatUsers(this.sessionId).then((data) => {
|
||||
this.chatProvider.getChatUsers(this.sessionId, {cmId: this.cmId}).then((data) => {
|
||||
this.users = data.users;
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhilegettingchatusers', true);
|
||||
|
|
|
@ -14,13 +14,14 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreUserProvider } from '@core/user/providers/user';
|
||||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
|
||||
import { AddonModChatMessageForView, AddonModChatSessionMessageForView } from './helper';
|
||||
import { CoreCourseCommonModWSOptions } from '@core/course/providers/course';
|
||||
|
||||
/**
|
||||
* Service that provides some features for chats.
|
||||
|
@ -40,17 +41,19 @@ export class AddonModChatProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param cmId Course module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the chat is retrieved.
|
||||
*/
|
||||
getChat(courseId: number, cmId: number, siteId?: string): Promise<AddonModChatChat> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getChat(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChatChat> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
const preSets = {
|
||||
cacheKey: this.getChatsCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModChatProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_chat_get_chats_by_courses', params, preSets)
|
||||
|
@ -179,17 +182,25 @@ export class AddonModChatProvider {
|
|||
* Get the actives users of a current chat.
|
||||
*
|
||||
* @param sessionId Chat sessiond ID.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the WS is executed.
|
||||
*/
|
||||
getChatUsers(sessionId: string): Promise<AddonModChatGetChatUsersResult> {
|
||||
const params = {
|
||||
chatsid: sessionId
|
||||
};
|
||||
const preSets = {
|
||||
getFromCache: false
|
||||
};
|
||||
getChatUsers(sessionId: string, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChatGetChatUsersResult> {
|
||||
// By default, always try to get the latest data.
|
||||
options.readingStrategy = options.readingStrategy || CoreSitesReadingStrategy.PreferNetwork;
|
||||
|
||||
return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets);
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
chatsid: sessionId,
|
||||
};
|
||||
const preSets = {
|
||||
component: AddonModChatProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_chat_get_chat_users', params, preSets);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -210,28 +221,26 @@ export class AddonModChatProvider {
|
|||
* @param chatId Chat ID.
|
||||
* @param groupId Group ID, 0 means that the function will determine the user group.
|
||||
* @param showAll Whether to include incomplete sessions or not.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the list of sessions.
|
||||
* @since 3.5
|
||||
*/
|
||||
getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, ignoreCache: boolean = false, siteId?: string):
|
||||
getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, options: CoreCourseCommonModWSOptions = {}):
|
||||
Promise<AddonModChatSession[]> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
chatid: chatId,
|
||||
groupid: groupId,
|
||||
showall: showAll ? 1 : 0
|
||||
showall: showAll ? 1 : 0,
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
const preSets = {
|
||||
cacheKey: this.getSessionsCacheKey(chatId, groupId, showAll),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
component: AddonModChatProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
return site.read('mod_chat_get_sessions', params, preSets).then((response: AddonModChatGetSessionsResult): any => {
|
||||
if (!response || !response.sessions) {
|
||||
|
@ -250,29 +259,27 @@ export class AddonModChatProvider {
|
|||
* @param sessionStart Session start time.
|
||||
* @param sessionEnd Session end time.
|
||||
* @param groupId Group ID, 0 means that the function will determine the user group.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return 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<AddonModChatSessionMessage[]> {
|
||||
getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0,
|
||||
options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChatSessionMessage[]> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
chatid: chatId,
|
||||
sessionstart: sessionStart,
|
||||
sessionend: sessionEnd,
|
||||
groupid: groupId
|
||||
groupid: groupId,
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
const preSets = {
|
||||
cacheKey: this.getSessionMessagesCacheKey(chatId, sessionStart, groupId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModChatProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
return site.read('mod_chat_get_session_messages', params, preSets)
|
||||
.then((response: AddonModChatGetSessionMessagesResult): any => {
|
||||
|
|
|
@ -17,7 +17,7 @@ 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 { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
|
@ -122,9 +122,14 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl
|
|||
protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||
// Prefetch chat and group info.
|
||||
const promises: Promise<any>[] = [
|
||||
this.chatProvider.getChat(courseId, module.id, siteId),
|
||||
this.chatProvider.getChat(courseId, module.id, {readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, siteId}),
|
||||
this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId)
|
||||
];
|
||||
const options = {
|
||||
cmId: module.id,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
return Promise.all(promises).then(([chat, groupInfo]: [AddonModChatChat, CoreGroupInfo]) => {
|
||||
const promises = [];
|
||||
|
@ -136,7 +141,7 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl
|
|||
|
||||
groupIds.forEach((groupId) => {
|
||||
// Prefetch complete sessions.
|
||||
promises.push(this.chatProvider.getSessions(chat.id, groupId, false, true, siteId).catch((error) => {
|
||||
promises.push(this.chatProvider.getSessions(chat.id, groupId, false, options).catch((error) => {
|
||||
// Ignore group error.
|
||||
if (error.errorcode != 'notingroup') {
|
||||
return Promise.reject(error);
|
||||
|
@ -144,8 +149,9 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl
|
|||
}));
|
||||
|
||||
// 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));
|
||||
promises.push(this.chatProvider.getSessions(chat.id, groupId, true, options).then((sessions) => {
|
||||
const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, module.id,
|
||||
siteId));
|
||||
|
||||
return Promise.all(promises);
|
||||
}).catch((error) => {
|
||||
|
@ -170,9 +176,13 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl
|
|||
* @param siteId Site ID.
|
||||
* @return 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) => {
|
||||
protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, cmId: number, siteId: string)
|
||||
: Promise<any> {
|
||||
return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, {
|
||||
cmId,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
}).then((messages) => {
|
||||
const users = {};
|
||||
session.sessionusers.forEach((user) => {
|
||||
users[user.userid] = true;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
|
@ -43,13 +43,17 @@
|
|||
<ion-card *ngIf="options && options.length">
|
||||
<ng-container *ngIf="choice.allowmultiple">
|
||||
<ion-item text-wrap *ngFor="let option of options">
|
||||
<ion-label><core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> <span *ngIf="choice.limitanswers && option.countanswers >= option.maxanswers">{{ 'addon.mod_choice.full' | translate }}</span></ion-label>
|
||||
<ion-label>
|
||||
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
|
||||
</ion-label>
|
||||
<ion-checkbox item-end [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!choice.allowmultiple">
|
||||
<ion-item text-wrap *ngFor="let option of options" radio-group [(ngModel)]="selectedOption.id">
|
||||
<ion-label><core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> <span *ngIf="choice.limitanswers && option.countanswers >= option.maxanswers">{{ 'addon.mod_choice.full' | translate }}</span></ion-label>
|
||||
<ion-label>
|
||||
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
|
||||
</ion-label>
|
||||
<ion-radio color="primary" [value]="option.id" [disabled]="option.disabled || !canEdit"></ion-radio>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
@ -81,6 +85,7 @@
|
|||
<ion-item-divider text-wrap>
|
||||
<h2><core-format-text [text]="result.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text></h2>
|
||||
<p>{{ 'addon.mod_choice.numberofuser' | translate }}: {{ result.numberofuser }} ({{ 'core.percentagenumber' | translate: {$a: result.percentageamountfixed} }})</p>
|
||||
<p *ngIf="choice.limitanswers && choice.showavailable">{{ 'addon.mod_choice.limita' | translate:{$a: result.maxanswer} }}</p>
|
||||
</ion-item-divider>
|
||||
<a ion-item *ngFor="let user of result.userresponses" core-user-link [courseId]="courseid" [userId]="user.userid" [title]="user.fullname" text-wrap>
|
||||
<ion-avatar core-user-avatar [user]="user" item-start [courseId]="courseid"></ion-avatar>
|
||||
|
@ -95,3 +100,14 @@
|
|||
<p>{{ 'addon.mod_choice.noresultsviewable' | translate }}</p>
|
||||
</ion-card>
|
||||
</core-loading>
|
||||
|
||||
<!-- Template to render a choice option label. -->
|
||||
<ng-template #optionLabelTemplate let-option="option">
|
||||
<p>
|
||||
<core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> <span *ngIf="choice.limitanswers && option.countanswers >= option.maxanswers">{{ 'addon.mod_choice.full' | translate }}</span>
|
||||
</p>
|
||||
<ng-container *ngIf="choice.limitanswers && choice.showavailable">
|
||||
<p>{{ 'addon.mod_choice.responsesa' | translate:{$a: option.countanswers} }}</p>
|
||||
<p>{{ 'addon.mod_choice.limita' | translate:{$a: option.maxanswers} }}</p>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
|
|
@ -174,7 +174,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected fetchOptions(hasOffline: boolean): Promise<any> {
|
||||
return this.choiceProvider.getOptions(this.choice.id).then((options) => {
|
||||
return this.choiceProvider.getOptions(this.choice.id, {cmId: this.module.id}).then((options) => {
|
||||
let promise;
|
||||
|
||||
// Check if the user has answered (synced) to allow show results.
|
||||
|
@ -294,7 +294,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.choiceProvider.getResults(this.choice.id).then((results) => {
|
||||
return this.choiceProvider.getResults(this.choice.id, {cmId: this.module.id}).then((results) => {
|
||||
let hasVotes = false;
|
||||
this.data = [];
|
||||
this.labels = [];
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"errorgetchoice": "Error getting choice data.",
|
||||
"expired": "This activity closed on {{$a}}.",
|
||||
"full": "(Full)",
|
||||
"limita": "Limit: {{$a}}",
|
||||
"modulenameplural": "Choices",
|
||||
"noresultsviewable": "The results are not currently viewable.",
|
||||
"notopenyet": "This activity is not available until {{$a}}.",
|
||||
|
@ -17,6 +18,7 @@
|
|||
"publishinfonever": "The results of this activity will not be published after you answer.",
|
||||
"removemychoice": "Remove my choice",
|
||||
"responses": "Responses",
|
||||
"responsesa": "Responses: {{$a}}",
|
||||
"responsesresultgraphdescription": "{{number}}% of the users chose the option: {{text}}.",
|
||||
"responsesresultgraphheader": "Graph display",
|
||||
"resultsnotsynced": "Your last response must be synchronised before it is included in the results.",
|
||||
|
|
|
@ -13,14 +13,15 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||
import { AddonModChoiceOfflineProvider } from './offline';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
|
||||
import { CoreCourseCommonModWSOptions } from '@core/course/providers/course';
|
||||
|
||||
/**
|
||||
* Service that provides some features for choices.
|
||||
|
@ -173,34 +174,26 @@ export class AddonModChoiceProvider {
|
|||
/**
|
||||
* Get a choice with key=value. If more than one is found, only the first will be returned.
|
||||
*
|
||||
* @param siteId Site ID.
|
||||
* @param courseId Course ID.
|
||||
* @param key Name of the property to check.
|
||||
* @param value Value to search.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the choice is retrieved.
|
||||
*/
|
||||
protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache?: boolean,
|
||||
ignoreCache?: boolean): Promise<AddonModChoiceChoice> {
|
||||
protected getChoiceByDataKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {})
|
||||
: Promise<AddonModChoiceChoice> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
const preSets = {
|
||||
cacheKey: this.getChoiceDataCacheKey(courseId),
|
||||
omitExpires: forceCache,
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModChoiceProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
if (forceCache) {
|
||||
preSets.omitExpires = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
return site.read('mod_choice_get_choices_by_courses', params, preSets)
|
||||
.then((response: AddonModChoiceGetChoicesByCoursesResult): any => {
|
||||
|
||||
|
@ -221,14 +214,11 @@ export class AddonModChoiceProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param cmId Course module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the choice is retrieved.
|
||||
*/
|
||||
getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean)
|
||||
: Promise<AddonModChoiceChoice> {
|
||||
return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache);
|
||||
getChoice(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChoiceChoice> {
|
||||
return this.getChoiceByDataKey(courseId, 'coursemodule', cmId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,39 +226,33 @@ export class AddonModChoiceProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param choiceId Choice ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the choice is retrieved.
|
||||
*/
|
||||
getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean)
|
||||
: Promise<AddonModChoiceChoice> {
|
||||
return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache);
|
||||
getChoiceById(courseId: number, choiceId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChoiceChoice> {
|
||||
return this.getChoiceByDataKey(courseId, 'id', choiceId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get choice options.
|
||||
*
|
||||
* @param choiceId Choice ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with choice options.
|
||||
*/
|
||||
getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModChoiceOption[]> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getOptions(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChoiceOption[]> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
choiceid: choiceId
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
const preSets = {
|
||||
cacheKey: this.getChoiceOptionsCacheKey(choiceId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModChoiceProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
return site.read('mod_choice_get_choice_options', params, preSets)
|
||||
.then((response: AddonModChoiceGetChoiceOptionsResult): any => {
|
||||
|
||||
|
@ -285,24 +269,21 @@ export class AddonModChoiceProvider {
|
|||
* Get choice results.
|
||||
*
|
||||
* @param choiceId Choice ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with choice results.
|
||||
*/
|
||||
getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise<AddonModChoiceResult[]> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getResults(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChoiceResult[]> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
choiceid: choiceId
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getChoiceResultsCacheKey(choiceId)
|
||||
const preSets = {
|
||||
cacheKey: this.getChoiceOptionsCacheKey(choiceId),
|
||||
component: AddonModChoiceProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
return site.read('mod_choice_get_choice_results', params, preSets)
|
||||
.then((response: AddonModChoiceGetChoiceResults): any => {
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core';
|
|||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
|
@ -79,12 +79,21 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected prefetchChoice(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||
return this.choiceProvider.getChoice(courseId, module.id, siteId, false, true).then((choice) => {
|
||||
const commonOptions = {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
const modOptions = {
|
||||
cmId: module.id,
|
||||
...commonOptions, // Include all common options.
|
||||
};
|
||||
|
||||
return this.choiceProvider.getChoice(courseId, module.id, commonOptions).then((choice) => {
|
||||
const promises = [];
|
||||
|
||||
// Get the options and results.
|
||||
promises.push(this.choiceProvider.getOptions(choice.id, true, siteId));
|
||||
promises.push(this.choiceProvider.getResults(choice.id, true, siteId).then((options) => {
|
||||
promises.push(this.choiceProvider.getOptions(choice.id, modOptions));
|
||||
promises.push(this.choiceProvider.getResults(choice.id, modOptions).then((options) => {
|
||||
// If we can see the users that answered, prefetch their profile and avatar.
|
||||
const subPromises = [];
|
||||
options.forEach((option) => {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<core-context-menu-item [priority]="500" *ngIf="canAdd" [content]="'addon.mod_data.addentries' | translate" [iconAction]="'add'" (action)="gotoAddEntries($event)"></core-context-menu-item>
|
||||
<core-context-menu-item [priority]="400" *ngIf="firstEntry" [content]="'addon.mod_data.single' | translate" [iconAction]="'document'" (action)="gotoEntry(firstEntry)"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="300" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
|
|
|
@ -180,69 +180,67 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
* @param showErrors If show errors to the user of hide them.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||
let canAdd = false,
|
||||
canSearch = false;
|
||||
|
||||
return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => {
|
||||
this.data = data;
|
||||
this.hasComments = data.comments;
|
||||
this.data = await this.dataProvider.getDatabase(this.courseId, this.module.id);
|
||||
this.hasComments = this.data.comments;
|
||||
|
||||
this.description = data.intro || data.description;
|
||||
this.dataRetrieved.emit(data);
|
||||
this.description = this.data.intro || this.data.description;
|
||||
this.dataRetrieved.emit(this.data);
|
||||
|
||||
if (sync) {
|
||||
if (sync) {
|
||||
try {
|
||||
// Try to synchronize the data.
|
||||
return this.syncActivity(showErrors).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
await this.syncActivity(showErrors);
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}).then(() => {
|
||||
return this.dataProvider.getDatabaseAccessInformation(this.data.id);
|
||||
}).then((accessData) => {
|
||||
this.access = accessData;
|
||||
}
|
||||
|
||||
if (!accessData.timeavailable) {
|
||||
const time = this.timeUtils.timestamp();
|
||||
this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.data.coursemodule);
|
||||
this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, this.groupInfo);
|
||||
|
||||
this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ?
|
||||
parseInt(this.data.timeavailablefrom, 10) * 1000 : false;
|
||||
this.timeAvailableFromReadable = this.timeAvailableFrom ? this.timeUtils.userDate(this.timeAvailableFrom) : false;
|
||||
this.timeAvailableTo = this.data.timeavailableto && time > this.data.timeavailableto ?
|
||||
parseInt(this.data.timeavailableto, 10) * 1000 : false;
|
||||
this.timeAvailableToReadable = this.timeAvailableTo ? this.timeUtils.userDate(this.timeAvailableTo) : false;
|
||||
this.access = await this.dataProvider.getDatabaseAccessInformation(this.data.id, {
|
||||
cmId: this.module.id,
|
||||
groupId: this.selectedGroup || undefined
|
||||
});
|
||||
|
||||
this.isEmpty = true;
|
||||
this.groupInfo = null;
|
||||
if (!this.access.timeavailable) {
|
||||
const time = this.timeUtils.timestamp();
|
||||
|
||||
return;
|
||||
}
|
||||
this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ?
|
||||
parseInt(this.data.timeavailablefrom, 10) * 1000 : false;
|
||||
this.timeAvailableFromReadable = this.timeAvailableFrom ? this.timeUtils.userDate(this.timeAvailableFrom) : false;
|
||||
this.timeAvailableTo = this.data.timeavailableto && time > this.data.timeavailableto ?
|
||||
parseInt(this.data.timeavailableto, 10) * 1000 : false;
|
||||
this.timeAvailableToReadable = this.timeAvailableTo ? this.timeUtils.userDate(this.timeAvailableTo) : false;
|
||||
|
||||
this.isEmpty = true;
|
||||
this.groupInfo = null;
|
||||
} else {
|
||||
canSearch = true;
|
||||
canAdd = accessData.canaddentry;
|
||||
canAdd = this.access.canaddentry;
|
||||
}
|
||||
|
||||
return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => {
|
||||
this.groupInfo = groupInfo;
|
||||
this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo);
|
||||
});
|
||||
}).then(() => {
|
||||
return this.dataProvider.getFields(this.data.id).then((fields) => {
|
||||
if (fields.length == 0) {
|
||||
canSearch = false;
|
||||
canAdd = false;
|
||||
}
|
||||
this.search.advanced = [];
|
||||
const fields = await this.dataProvider.getFields(this.data.id, {cmId: this.module.id});
|
||||
this.search.advanced = [];
|
||||
|
||||
this.fields = this.utils.arrayToObject(fields, 'id');
|
||||
this.fieldsArray = this.utils.objectToArray(this.fields);
|
||||
this.fields = this.utils.arrayToObject(fields, 'id');
|
||||
this.fieldsArray = this.utils.objectToArray(this.fields);
|
||||
if (this.fieldsArray.length == 0) {
|
||||
canSearch = false;
|
||||
canAdd = false;
|
||||
}
|
||||
|
||||
return this.fetchEntriesData();
|
||||
});
|
||||
}).finally(() => {
|
||||
try {
|
||||
await this.fetchEntriesData();
|
||||
} finally {
|
||||
this.canAdd = canAdd;
|
||||
this.canSearch = canSearch;
|
||||
this.fillContextMenu(refresh);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -252,15 +250,17 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
*/
|
||||
protected fetchEntriesData(): Promise<any> {
|
||||
|
||||
return this.dataProvider.getDatabaseAccessInformation(this.data.id, this.selectedGroup).then((accessData) => {
|
||||
// Update values for current group.
|
||||
this.access.canaddentry = accessData.canaddentry;
|
||||
const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined;
|
||||
const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined;
|
||||
|
||||
const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined;
|
||||
const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined;
|
||||
|
||||
return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, search, advSearch,
|
||||
this.search.sortBy, this.search.sortDirection, this.search.page);
|
||||
return this.dataHelper.fetchEntries(this.data, this.fieldsArray, {
|
||||
groupId: this.selectedGroup,
|
||||
search,
|
||||
advSearch,
|
||||
sort: Number(this.search.sortBy),
|
||||
order: this.search.sortDirection,
|
||||
page: this.search.page,
|
||||
cmId: this.module.id,
|
||||
}).then((entries) => {
|
||||
const numEntries = entries.entries.length;
|
||||
const numOfflineEntries = entries.offlineEntries.length;
|
||||
|
@ -381,18 +381,29 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
* @param groupId Group ID.
|
||||
* @return Resolved when new group is selected or rejected if not.
|
||||
*/
|
||||
setGroup(groupId: number): Promise<any> {
|
||||
async setGroup(groupId: number): Promise<void> {
|
||||
this.selectedGroup = groupId;
|
||||
this.search.page = 0;
|
||||
|
||||
return this.fetchEntriesData().then(() => {
|
||||
// Only update canAdd if there's any field, otheerwise, canAdd will remain false.
|
||||
if (this.fieldsArray.length > 0) {
|
||||
// Update values for current group.
|
||||
this.access = await this.dataProvider.getDatabaseAccessInformation(this.data.id, {
|
||||
groupId: this.selectedGroup,
|
||||
cmId: this.module.id,
|
||||
});
|
||||
|
||||
this.canAdd = this.access.canaddentry;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.fetchEntriesData();
|
||||
|
||||
// Log activity view for coherence with Moodle web.
|
||||
return this.logView();
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
} catch (error) {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,10 +14,9 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { Platform } from 'ionic-angular';
|
||||
import { Geolocation, GeolocationOptions } from '@ionic-native/geolocation';
|
||||
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreApp, CoreAppProvider } from '@providers/app';
|
||||
import { CoreGeolocation, CoreGeolocationError, CoreGeolocationErrorReason } from '@providers/geolocation';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
|
||||
/**
|
||||
|
@ -33,15 +32,14 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo
|
|||
east: number;
|
||||
showGeolocation: boolean;
|
||||
|
||||
constructor(protected fb: FormBuilder,
|
||||
protected platform: Platform,
|
||||
protected geolocation: Geolocation,
|
||||
constructor(
|
||||
protected fb: FormBuilder,
|
||||
protected domUtils: CoreDomUtilsProvider,
|
||||
protected sanitizer: DomSanitizer,
|
||||
protected appProvider: CoreAppProvider) {
|
||||
appProvider: CoreAppProvider) {
|
||||
super(fb);
|
||||
|
||||
this.showGeolocation = !this.appProvider.isDesktop();
|
||||
this.showGeolocation = !appProvider.isDesktop();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,7 +71,7 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo
|
|||
const eastFixed = east ? east.toFixed(4) : '0.0000';
|
||||
let url;
|
||||
|
||||
if (this.platform.is('ios')) {
|
||||
if (CoreApp.instance.isIOS()) {
|
||||
url = 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed;
|
||||
} else {
|
||||
url = 'geo:' + northFixed + ',' + eastFixed;
|
||||
|
@ -115,33 +113,51 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo
|
|||
*
|
||||
* @param $event The event.
|
||||
*/
|
||||
getLocation(event: Event): void {
|
||||
async getLocation(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const modal = this.domUtils.showModalLoading('addon.mod_data.gettinglocation', true);
|
||||
|
||||
const options: GeolocationOptions = {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 30000
|
||||
};
|
||||
try {
|
||||
const coordinates = await CoreGeolocation.instance.getCoordinates();
|
||||
|
||||
this.geolocation.getCurrentPosition(options).then((result) => {
|
||||
this.form.controls['f_' + this.field.id + '_0'].setValue(result.coords.latitude);
|
||||
this.form.controls['f_' + this.field.id + '_1'].setValue(result.coords.longitude);
|
||||
}).catch((error) => {
|
||||
if (this.isPermissionDeniedError(error)) {
|
||||
this.domUtils.showErrorModal('addon.mod_data.locationpermissiondenied', true);
|
||||
this.form.controls['f_' + this.field.id + '_0'].setValue(coordinates.latitude);
|
||||
this.form.controls['f_' + this.field.id + '_1'].setValue(coordinates.longitude);
|
||||
} catch (error) {
|
||||
this.showLocationErrorModal(error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting location');
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
modal.dismiss();
|
||||
}
|
||||
|
||||
protected isPermissionDeniedError(error?: any): boolean {
|
||||
return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED;
|
||||
/**
|
||||
* Show the appropriate error modal for the given error getting the location.
|
||||
*
|
||||
* @param error Location error.
|
||||
*/
|
||||
protected showLocationErrorModal(error: any): void {
|
||||
if (error instanceof CoreGeolocationError) {
|
||||
this.domUtils.showErrorModal(this.getGeolocationErrorMessage(error), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting location');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error message from a geolocation error.
|
||||
*
|
||||
* @param error Geolocation error.
|
||||
*/
|
||||
protected getGeolocationErrorMessage(error: CoreGeolocationError): string {
|
||||
// tslint:disable-next-line: switch-default
|
||||
switch (error.reason) {
|
||||
case CoreGeolocationErrorReason.PermissionDenied:
|
||||
return 'addon.mod_data.locationpermissiondenied';
|
||||
case CoreGeolocationErrorReason.LocationNotEnabled:
|
||||
return 'addon.mod_data.locationnotenabled';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,10 +23,12 @@
|
|||
"gettinglocation": "Getting location",
|
||||
"latlongboth": "Both latitude and longitude are required.",
|
||||
"locationpermissiondenied": "Permission to access your location has been denied.",
|
||||
"locationnotenabled": "Location is not enabled",
|
||||
"menuchoose": "Choose...",
|
||||
"modulenameplural": "Databases",
|
||||
"more": "More",
|
||||
"mylocation": "My location",
|
||||
"noaccess": "You do not have access to this page",
|
||||
"nomatch": "No matching entries found!",
|
||||
"norecords": "No entries in database",
|
||||
"notapproved": "Entry is not approved yet.",
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<div class="addon-data-contents addon-data-entries-{{data.id}}" *ngIf="data">
|
||||
<core-style [css]="data.csstemplate" prefix=".addon-data-entries-{{data.id}}"></core-style>
|
||||
<div class="addon-data-contents {{cssClass}}" *ngIf="data">
|
||||
<core-style [css]="data.csstemplate" prefix=".{{cssClass}}"></core-style>
|
||||
|
||||
<form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl>
|
||||
<core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
|
||||
|
|
|
@ -52,6 +52,8 @@ export class AddonModDataEditPage {
|
|||
protected siteId: string;
|
||||
protected offline: boolean;
|
||||
protected forceLeave = false; // To allow leaving the page without checking for changes.
|
||||
protected initialSelectedGroup = null;
|
||||
protected isEditing = false;
|
||||
|
||||
title = '';
|
||||
component = AddonModDataProvider.COMPONENT;
|
||||
|
@ -75,7 +77,10 @@ export class AddonModDataEditPage {
|
|||
this.module = params.get('module') || {};
|
||||
this.entryId = params.get('entryId') || null;
|
||||
this.courseId = params.get('courseId');
|
||||
this.selectedGroup = params.get('group') || 0;
|
||||
this.selectedGroup = this.entryId ? null : (params.get('group') || 0);
|
||||
|
||||
// If entryId is lower than 0 or null, it is a new entry or an offline entry.
|
||||
this.isEditing = this.entryId && this.entryId > 0;
|
||||
|
||||
this.siteId = sitesProvider.getCurrentSiteId();
|
||||
|
||||
|
@ -88,7 +93,7 @@ export class AddonModDataEditPage {
|
|||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad(): void {
|
||||
this.fetchEntryData();
|
||||
this.fetchEntryData(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,7 +108,8 @@ export class AddonModDataEditPage {
|
|||
|
||||
const inputData = this.editForm.value;
|
||||
|
||||
const changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents);
|
||||
let changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents);
|
||||
changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup);
|
||||
|
||||
if (changed) {
|
||||
// Show confirmation if some data has been modified.
|
||||
|
@ -120,38 +126,78 @@ export class AddonModDataEditPage {
|
|||
/**
|
||||
* Fetch the entry data.
|
||||
*
|
||||
* @param [refresh] To refresh all downloaded data.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected fetchEntryData(): Promise<any> {
|
||||
return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => {
|
||||
this.title = data.name || this.title;
|
||||
this.data = data;
|
||||
this.cssClass = 'addon-data-entries-' + data.id;
|
||||
protected async fetchEntryData(refresh: boolean = false): Promise<void> {
|
||||
try {
|
||||
this.data = await this.dataProvider.getDatabase(this.courseId, this.module.id);
|
||||
this.title = this.data.name || this.title;
|
||||
this.cssClass = 'addon-data-entries-' + this.data.id;
|
||||
|
||||
return this.dataProvider.getDatabaseAccessInformation(data.id);
|
||||
}).then((accessData) => {
|
||||
if (this.entryId) {
|
||||
return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => {
|
||||
this.groupInfo = groupInfo;
|
||||
this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo);
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
return this.dataProvider.getFields(this.data.id);
|
||||
}).then((fieldsData) => {
|
||||
this.fieldsArray = fieldsData;
|
||||
this.fields = this.utils.arrayToObject(fieldsData, 'id');
|
||||
this.fieldsArray = await this.dataProvider.getFields(this.data.id, {cmId: this.module.id});
|
||||
this.fields = this.utils.arrayToObject(this.fieldsArray, 'id');
|
||||
|
||||
const entry = await this.dataHelper.fetchEntry(this.data, this.fieldsArray, this.entryId);
|
||||
|
||||
return this.dataHelper.fetchEntry(this.data, fieldsData, this.entryId);
|
||||
}).then((entry) => {
|
||||
this.entry = entry.entry;
|
||||
|
||||
// Load correct group.
|
||||
this.selectedGroup = this.selectedGroup == null ? this.entry.groupid : this.selectedGroup;
|
||||
|
||||
// Check permissions when adding a new entry or offline entry.
|
||||
if (!this.isEditing) {
|
||||
let haveAccess = false;
|
||||
|
||||
if (refresh) {
|
||||
this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.data.coursemodule);
|
||||
this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, this.groupInfo);
|
||||
this.initialSelectedGroup = this.selectedGroup;
|
||||
}
|
||||
|
||||
if (this.groupInfo.groups.length > 0) {
|
||||
if (refresh) {
|
||||
const canAddGroup = {};
|
||||
|
||||
await Promise.all(this.groupInfo.groups.map(async (group) => {
|
||||
const accessData = await this.dataProvider.getDatabaseAccessInformation(this.data.id, {
|
||||
cmId: this.module.id, groupId: group.id});
|
||||
|
||||
canAddGroup[group.id] = accessData.canaddentry;
|
||||
}));
|
||||
|
||||
this.groupInfo.groups = this.groupInfo.groups.filter((group) => {
|
||||
return !!canAddGroup[group.id];
|
||||
});
|
||||
|
||||
haveAccess = canAddGroup[this.selectedGroup];
|
||||
} else {
|
||||
// Groups already filtered, so it have access.
|
||||
haveAccess = true;
|
||||
}
|
||||
} else {
|
||||
const accessData = await this.dataProvider.getDatabaseAccessInformation(this.data.id, {cmId: this.module.id});
|
||||
haveAccess = accessData.canaddentry;
|
||||
}
|
||||
|
||||
if (!haveAccess) {
|
||||
// You shall not pass, go back.
|
||||
this.domUtils.showErrorModal('addon.mod_data.noaccess', true);
|
||||
|
||||
// Go back to entry list.
|
||||
this.forceLeave = true;
|
||||
this.navCtrl.pop();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.editFormRender = this.displayEditFields();
|
||||
}).catch((message) => {
|
||||
} catch (message) {
|
||||
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,7 +206,7 @@ export class AddonModDataEditPage {
|
|||
* @param e Event.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
save(e: Event): Promise<any> {
|
||||
save(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -169,6 +215,7 @@ export class AddonModDataEditPage {
|
|||
return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id,
|
||||
this.entry.contents).then((changed) => {
|
||||
|
||||
changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup);
|
||||
if (!changed) {
|
||||
if (this.entryId) {
|
||||
return this.returnToEntryList();
|
||||
|
@ -196,7 +243,7 @@ export class AddonModDataEditPage {
|
|||
return Promise.reject(e);
|
||||
}).then((editData) => {
|
||||
if (editData.length > 0) {
|
||||
if (this.entryId) {
|
||||
if (this.isEditing) {
|
||||
return this.dataProvider.editEntry(this.data.id, this.entryId, this.courseId, editData, this.fields,
|
||||
undefined, this.offline);
|
||||
}
|
||||
|
@ -213,20 +260,20 @@ export class AddonModDataEditPage {
|
|||
}
|
||||
|
||||
// This is done if entry is updated when editing or creating if not.
|
||||
if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) {
|
||||
if ((this.isEditing && result.updated) || (!this.isEditing && result.newentryid)) {
|
||||
|
||||
this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.siteId);
|
||||
|
||||
if (result.sent) {
|
||||
this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' });
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
|
||||
this.entryId = this.entryId || result.newentryid;
|
||||
if (result.sent) {
|
||||
this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' });
|
||||
|
||||
promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId));
|
||||
promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId));
|
||||
if (this.isEditing) {
|
||||
promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId));
|
||||
}
|
||||
promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED,
|
||||
|
@ -264,7 +311,7 @@ export class AddonModDataEditPage {
|
|||
* @param groupId Group identifier to set.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
setGroup(groupId: number): Promise<any> {
|
||||
setGroup(groupId: number): Promise<void> {
|
||||
this.selectedGroup = groupId;
|
||||
this.loaded = false;
|
||||
|
||||
|
@ -322,7 +369,7 @@ export class AddonModDataEditPage {
|
|||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected returnToEntryList(): Promise<any> {
|
||||
protected returnToEntryList(): Promise<void> {
|
||||
const inputData = this.editForm.value;
|
||||
|
||||
return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id,
|
||||
|
|
|
@ -142,13 +142,13 @@ export class AddonModDataEntryPage implements OnDestroy {
|
|||
this.title = data.name || this.title;
|
||||
this.data = data;
|
||||
|
||||
return this.dataProvider.getFields(this.data.id).then((fieldsData) => {
|
||||
return this.dataProvider.getFields(this.data.id, {cmId: this.module.id}).then((fieldsData) => {
|
||||
this.fields = this.utils.arrayToObject(fieldsData, 'id');
|
||||
this.fieldsArray = fieldsData;
|
||||
});
|
||||
}).then(() => {
|
||||
return this.setEntryFromOffset().then(() => {
|
||||
return this.dataProvider.getDatabaseAccessInformation(this.data.id);
|
||||
return this.dataProvider.getDatabaseAccessInformation(this.data.id, {cmId: this.module.id});
|
||||
});
|
||||
}).then((accessData) => {
|
||||
this.access = accessData;
|
||||
|
@ -290,8 +290,13 @@ export class AddonModDataEntryPage implements OnDestroy {
|
|||
const perPage = AddonModDataProvider.PER_PAGE;
|
||||
const page = !emptyOffset && this.offset >= 0 ? Math.floor(this.offset / perPage) : 0;
|
||||
|
||||
return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, undefined, undefined, '0', 'DESC',
|
||||
page, perPage).then((entries) => {
|
||||
return this.dataHelper.fetchEntries(this.data, this.fieldsArray, {
|
||||
groupId: this.selectedGroup,
|
||||
sort: 0,
|
||||
order: 'DESC',
|
||||
page,
|
||||
perPage,
|
||||
}).then((entries) => {
|
||||
|
||||
const pageEntries = entries.offlineEntries.concat(entries.entries);
|
||||
let pageIndex; // Index of the entry when concatenating offline and online page entries.
|
||||
|
@ -321,8 +326,11 @@ export class AddonModDataEntryPage implements OnDestroy {
|
|||
this.nextOffset = null;
|
||||
} else {
|
||||
// Last entry of the page, check if there are more pages.
|
||||
promise = this.dataProvider.getEntries(this.data.id, this.selectedGroup, '0', 'DESC', page + 1, perPage)
|
||||
.then((entries) => {
|
||||
promise = this.dataProvider.getEntries(this.data.id, {
|
||||
groupId: this.selectedGroup,
|
||||
page: page + 1,
|
||||
perPage: perPage,
|
||||
}).then((entries) => {
|
||||
this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? this.offset + 1 : null;
|
||||
});
|
||||
}
|
||||
|
@ -330,7 +338,7 @@ export class AddonModDataEntryPage implements OnDestroy {
|
|||
return Promise.resolve(promise).then(() => {
|
||||
if (this.entryId > 0) {
|
||||
// Online entry, we need to fetch the the rating info.
|
||||
return this.dataProvider.getEntry(this.data.id, this.entryId).then((entry) => {
|
||||
return this.dataProvider.getEntry(this.data.id, this.entryId, {cmId: this.module.id}).then((entry) => {
|
||||
this.ratingInfo = entry.ratinginfo;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||
|
@ -23,6 +23,7 @@ import { AddonModDataOfflineProvider } from './offline';
|
|||
import { AddonModDataFieldsDelegate } from './fields-delegate';
|
||||
import { CoreRatingInfo } from '@core/rating/providers/rating';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreCourseCommonModWSOptions } from '@core/course/providers/course';
|
||||
|
||||
/**
|
||||
* Database entry (online or offline).
|
||||
|
@ -116,46 +117,51 @@ export class AddonModDataProvider {
|
|||
* @param forceOffline Force editing entry in offline.
|
||||
* @return Promise resolved when the action is done.
|
||||
*/
|
||||
addEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], groupId: number = 0,
|
||||
async addEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], groupId: number = 0,
|
||||
fields: any, siteId?: string, forceOffline: boolean = false): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Convenience function to store a data to be synchronized later.
|
||||
const storeOffline = (): Promise<any> => {
|
||||
return this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId)
|
||||
.then((entry) => {
|
||||
return {
|
||||
// Return provissional entry Id.
|
||||
newentryid: entry,
|
||||
sent: false,
|
||||
};
|
||||
});
|
||||
const storeOffline = async (): Promise<any> => {
|
||||
const entry = await this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId);
|
||||
|
||||
return {
|
||||
// Return provissional entry Id.
|
||||
newentryid: entry,
|
||||
sent: false,
|
||||
};
|
||||
};
|
||||
|
||||
// Checks to store offline.
|
||||
if (!this.appProvider.isOnline() || forceOffline) {
|
||||
const notifications = this.checkFields(fields, contents);
|
||||
if (notifications) {
|
||||
return Promise.resolve({
|
||||
fieldnotifications: notifications
|
||||
});
|
||||
return { fieldnotifications: notifications };
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unnecessary not synced actions.
|
||||
await this.deleteEntryOfflineAction(dataId, entryId, 'add', siteId);
|
||||
|
||||
// App is offline, store the action.
|
||||
if (!this.appProvider.isOnline() || forceOffline) {
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
return this.addEntryOnline(dataId, contents, groupId, siteId).then((result) => {
|
||||
try {
|
||||
const result = await this.addEntryOnline(dataId, contents, groupId, siteId);
|
||||
result.sent = true;
|
||||
|
||||
return result;
|
||||
}).catch((error) => {
|
||||
} catch (error) {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
return Promise.reject(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Couldn't connect to server, store in offline.
|
||||
return storeOffline();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,48 +198,49 @@ export class AddonModDataProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the action is done.
|
||||
*/
|
||||
approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise<any> {
|
||||
async approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Convenience function to store a data to be synchronized later.
|
||||
const storeOffline = (): Promise<any> => {
|
||||
const storeOffline = async (): Promise<any> => {
|
||||
const action = approve ? 'approve' : 'disapprove';
|
||||
|
||||
return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId)
|
||||
.then(() => {
|
||||
return {
|
||||
sent: false,
|
||||
};
|
||||
});
|
||||
await this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId);
|
||||
|
||||
return {
|
||||
sent: false,
|
||||
};
|
||||
};
|
||||
|
||||
// Get if the opposite action is not synced.
|
||||
const oppositeAction = approve ? 'disapprove' : 'approve';
|
||||
|
||||
return this.dataOffline.getEntry(dataId, entryId, oppositeAction, siteId).then(() => {
|
||||
// Found. Just delete the action.
|
||||
return this.dataOffline.deleteEntry(dataId, entryId, oppositeAction, siteId);
|
||||
}).catch(() => {
|
||||
const found = await this.deleteEntryOfflineAction(dataId, entryId, oppositeAction, siteId);
|
||||
if (found) {
|
||||
// Offline action has been found and deleted. Stop here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.approveEntryOnline(entryId, approve, siteId);
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.approveEntryOnline(entryId, approve, siteId).then(() => {
|
||||
return {
|
||||
sent: true,
|
||||
};
|
||||
}).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Couldn't connect to server, store in offline.
|
||||
return storeOffline();
|
||||
});
|
||||
});
|
||||
// Couldn't connect to server, store in offline.
|
||||
return storeOffline();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -297,60 +304,45 @@ export class AddonModDataProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the action is done.
|
||||
*/
|
||||
deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise<any> {
|
||||
async deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Convenience function to store a data to be synchronized later.
|
||||
const storeOffline = (): Promise<any> => {
|
||||
return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId)
|
||||
.then(() => {
|
||||
return {
|
||||
sent: false,
|
||||
};
|
||||
});
|
||||
const storeOffline = async (): Promise<any> => {
|
||||
await this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId);
|
||||
|
||||
return {
|
||||
sent: false,
|
||||
};
|
||||
};
|
||||
|
||||
let justAdded = false;
|
||||
|
||||
// Check if the opposite action is not synced and just delete it.
|
||||
return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => {
|
||||
if (entries && entries.length) {
|
||||
// Found. Delete other actions first.
|
||||
const proms = entries.map((entry) => {
|
||||
if (entry.action == 'add') {
|
||||
justAdded = true;
|
||||
}
|
||||
const addedOffline = await this.deleteEntryOfflineAction(dataId, entryId, 'add', siteId);
|
||||
if (addedOffline) {
|
||||
// Offline add action found and deleted. Stop here.
|
||||
return;
|
||||
}
|
||||
|
||||
return this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId);
|
||||
});
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
return Promise.all(proms);
|
||||
}
|
||||
}).then(() => {
|
||||
if (justAdded) {
|
||||
// The field was added offline, delete and stop.
|
||||
return;
|
||||
try {
|
||||
await this.deleteEntryOnline(entryId, siteId);
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
return this.deleteEntryOnline(entryId, siteId).then(() => {
|
||||
return {
|
||||
sent: true,
|
||||
};
|
||||
}).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Couldn't connect to server, store in offline.
|
||||
return storeOffline();
|
||||
});
|
||||
});
|
||||
// Couldn't connect to server, store in offline.
|
||||
return storeOffline();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -370,6 +362,29 @@ export class AddonModDataProvider {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete entry offline action.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param entryId Entry ID.
|
||||
* @param action Action name to delete.
|
||||
* @param siteId Site ID.
|
||||
* @return Resolved with true if the action has been found and deleted.
|
||||
*/
|
||||
protected async deleteEntryOfflineAction(dataId: number, entryId: number, action: string, siteId: string): Promise<boolean> {
|
||||
// Get other not not synced actions.
|
||||
try {
|
||||
await this.dataOffline.getEntry(dataId, entryId, action, siteId);
|
||||
|
||||
await this.dataOffline.deleteEntry(dataId, entryId, action, siteId);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Not found.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing entry.
|
||||
*
|
||||
|
@ -382,82 +397,50 @@ export class AddonModDataProvider {
|
|||
* @param forceOffline Force editing entry in offline.
|
||||
* @return Promise resolved when the action is done.
|
||||
*/
|
||||
editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, siteId?: string,
|
||||
forceOffline: boolean = false): Promise<any> {
|
||||
async editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any,
|
||||
siteId?: string, forceOffline: boolean = false): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Convenience function to store a data to be synchronized later.
|
||||
const storeOffline = (): Promise<any> => {
|
||||
return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId)
|
||||
.then(() => {
|
||||
return {
|
||||
updated: true,
|
||||
sent: false,
|
||||
};
|
||||
});
|
||||
};
|
||||
const storeOffline = async (): Promise<any> => {
|
||||
await this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId);
|
||||
|
||||
let justAdded = false,
|
||||
groupId;
|
||||
return {
|
||||
updated: true,
|
||||
sent: false,
|
||||
};
|
||||
};
|
||||
|
||||
if (!this.appProvider.isOnline() || forceOffline) {
|
||||
const notifications = this.checkFields(fields, contents);
|
||||
if (notifications) {
|
||||
return Promise.resolve({
|
||||
fieldnotifications: notifications
|
||||
});
|
||||
return { fieldnotifications: notifications };
|
||||
}
|
||||
}
|
||||
|
||||
// Get other not not synced actions.
|
||||
return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => {
|
||||
if (entries && entries.length) {
|
||||
// Found. Delete add and edit actions first.
|
||||
const proms = [];
|
||||
entries.forEach((entry) => {
|
||||
if (entry.action == 'add') {
|
||||
justAdded = true;
|
||||
groupId = entry.groupid;
|
||||
proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId));
|
||||
} else if (entry.action == 'edit') {
|
||||
proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId));
|
||||
}
|
||||
});
|
||||
// Remove unnecessary not synced actions.
|
||||
await this.deleteEntryOfflineAction(dataId, entryId, 'edit', siteId);
|
||||
|
||||
return Promise.all(proms);
|
||||
}
|
||||
}).then(() => {
|
||||
if (justAdded) {
|
||||
// The field was added offline, add again and stop.
|
||||
return this.addEntry(dataId, entryId, courseId, contents, groupId, fields, siteId, forceOffline)
|
||||
.then((result) => {
|
||||
result.updated = true;
|
||||
result.sent = true;
|
||||
if (!this.appProvider.isOnline() || forceOffline) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
try {
|
||||
const result = await this.editEntryOnline(entryId, contents, siteId);
|
||||
result.sent = true;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!this.appProvider.isOnline() || forceOffline) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
return this.editEntryOnline(entryId, contents, siteId).then((result) => {
|
||||
result.sent = true;
|
||||
|
||||
return result;
|
||||
}).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Couldn't connect to server, store in offline.
|
||||
return storeOffline();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Couldn't connect to server, store in offline.
|
||||
return storeOffline();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect.
|
||||
|
@ -482,49 +465,34 @@ export class AddonModDataProvider {
|
|||
* Performs the whole fetch of the entries in the database.
|
||||
*
|
||||
* @param dataId Data ID.
|
||||
* @param groupId Group ID.
|
||||
* @param sort Sort the records by this field id. See AddonModDataProvider#getEntries for more info.
|
||||
* @param order The direction of the sorting. See AddonModDataProvider#getEntries for more info.
|
||||
* @param perPage Records per page to fetch. It has to match with the prefetch.
|
||||
* Default on AddonModDataProvider.PER_PAGE.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
fetchAllEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC',
|
||||
perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false,
|
||||
siteId?: string): Promise<AddonModDataEntry[]> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
fetchAllEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise<AddonModDataEntry[]> {
|
||||
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
||||
options.page = 0;
|
||||
|
||||
return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId);
|
||||
return this.fetchEntriesRecursive(dataId, [], options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive call on fetch all entries.
|
||||
*
|
||||
* @param dataId Data ID.
|
||||
* @param groupId Group ID.
|
||||
* @param sort Sort the records by this field id. See AddonModDataProvider#getEntries for more info.
|
||||
* @param order The direction of the sorting. See AddonModDataProvider#getEntries for more info.
|
||||
* @param perPage Records per page to fetch. It has to match with the prefetch.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param entries Entries already fetch (just to concatenate them).
|
||||
* @param page Page of records to return.
|
||||
* @param siteId Site ID.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected fetchEntriesRecursive(dataId: number, groupId: number, sort: string, order: string, perPage: number,
|
||||
forceCache: boolean, ignoreCache: boolean, entries: any, page: number, siteId: string): Promise<AddonModDataEntry[]> {
|
||||
return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId)
|
||||
.then((result) => {
|
||||
protected fetchEntriesRecursive(dataId: number, entries: any, options: AddonModDataGetEntriesOptions = {})
|
||||
: Promise<AddonModDataEntry[]> {
|
||||
return this.getEntries(dataId, options).then((result) => {
|
||||
entries = entries.concat(result.entries);
|
||||
|
||||
const canLoadMore = perPage > 0 && ((page + 1) * perPage) < result.totalcount;
|
||||
const canLoadMore = options.perPage > 0 && ((options.page + 1) * options.perPage) < result.totalcount;
|
||||
if (canLoadMore) {
|
||||
return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, entries, page + 1,
|
||||
siteId);
|
||||
options.page++;
|
||||
|
||||
return this.fetchEntriesRecursive(dataId, entries, options);
|
||||
}
|
||||
|
||||
return entries;
|
||||
|
@ -557,23 +525,21 @@ export class AddonModDataProvider {
|
|||
* @param courseId Course ID.
|
||||
* @param key Name of the property to check.
|
||||
* @param value Value to search.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the data is retrieved.
|
||||
*/
|
||||
protected getDatabaseByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false):
|
||||
protected getDatabaseByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}):
|
||||
Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getDatabaseDataCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
};
|
||||
if (forceCache) {
|
||||
preSets['omitExpires'] = true;
|
||||
}
|
||||
courseids: [courseId],
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getDatabaseDataCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModDataProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_data_get_databases_by_courses', params, preSets).then((response) => {
|
||||
if (response && response.databases) {
|
||||
|
@ -593,12 +559,11 @@ export class AddonModDataProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param cmId Course module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the data is retrieved.
|
||||
*/
|
||||
getDatabase(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise<any> {
|
||||
return this.getDatabaseByKey(courseId, 'coursemodule', cmId, siteId, forceCache);
|
||||
getDatabase(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
||||
return this.getDatabaseByKey(courseId, 'coursemodule', cmId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -606,12 +571,11 @@ export class AddonModDataProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param id Data ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the data is retrieved.
|
||||
*/
|
||||
getDatabaseById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise<any> {
|
||||
return this.getDatabaseByKey(courseId, 'id', id, siteId, forceCache);
|
||||
getDatabaseById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
||||
return this.getDatabaseByKey(courseId, 'id', id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -639,32 +603,23 @@ export class AddonModDataProvider {
|
|||
* Get access information for a given database.
|
||||
*
|
||||
* @param dataId Data ID.
|
||||
* @param groupId Group ID.
|
||||
* @param offline True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the database is retrieved.
|
||||
*/
|
||||
getDatabaseAccessInformation(dataId: number, groupId?: number, offline: boolean = false, ignoreCache: boolean = false,
|
||||
siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getDatabaseAccessInformation(dataId: number, options: AddonModDataAccessInfoOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
options.groupId = options.groupId || 0;
|
||||
|
||||
const params = {
|
||||
databaseid: dataId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, groupId)
|
||||
};
|
||||
|
||||
if (typeof groupId !== 'undefined') {
|
||||
params['groupid'] = groupId;
|
||||
}
|
||||
|
||||
if (offline) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
databaseid: dataId,
|
||||
groupid: options.groupId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, options.groupId),
|
||||
component: AddonModDataProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_data_get_data_access_information', params, preSets);
|
||||
});
|
||||
|
@ -674,48 +629,34 @@ export class AddonModDataProvider {
|
|||
* Get entries for a specific database and group.
|
||||
*
|
||||
* @param dataId Data ID.
|
||||
* @param groupId Group ID.
|
||||
* @param sort Sort the records by this field id, reserved ids are:
|
||||
* 0: timeadded
|
||||
* -1: firstname
|
||||
* -2: lastname
|
||||
* -3: approved
|
||||
* -4: timemodified.
|
||||
* Empty for using the default database setting.
|
||||
* @param order The direction of the sorting: 'ASC' or 'DESC'.
|
||||
* Empty for using the default database setting.
|
||||
* @param page Page of records to return.
|
||||
* @param perPage Records per page to return. Default on PER_PAGE.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the database is retrieved.
|
||||
*/
|
||||
getEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', page: number = 0,
|
||||
perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false,
|
||||
siteId?: string): Promise<AddonModDataEntries> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise<AddonModDataEntries> {
|
||||
options.groupId = options.groupId || 0;
|
||||
options.sort = options.sort || 0;
|
||||
options.order = options.order || 'DESC';
|
||||
options.page = options.page || 0;
|
||||
options.perPage = options.perPage || AddonModDataProvider.PER_PAGE;
|
||||
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
// Always use sort and order params to improve cache usage (entries are identified by params).
|
||||
const params = {
|
||||
databaseid: dataId,
|
||||
returncontents: 1,
|
||||
page: page,
|
||||
perpage: perPage,
|
||||
groupid: groupId,
|
||||
sort: sort,
|
||||
order: order
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getEntriesCacheKey(dataId, groupId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
|
||||
};
|
||||
|
||||
if (forceCache) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
databaseid: dataId,
|
||||
returncontents: 1,
|
||||
page: options.page,
|
||||
perpage: options.perPage,
|
||||
groupid: options.groupId,
|
||||
sort: options.sort,
|
||||
order: options.order,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getEntriesCacheKey(dataId, options.groupId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
component: AddonModDataProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_data_get_entries', params, preSets).then((response) => {
|
||||
response.entries.forEach((entry) => {
|
||||
|
@ -753,26 +694,23 @@ export class AddonModDataProvider {
|
|||
*
|
||||
* @param dataId Data ID for caching purposes.
|
||||
* @param entryId Entry ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the entry is retrieved.
|
||||
*/
|
||||
getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string):
|
||||
getEntry(dataId: number, entryId: number, options: CoreCourseCommonModWSOptions = {}):
|
||||
Promise<{entry: AddonModDataEntry, ratinginfo: CoreRatingInfo}> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
entryid: entryId,
|
||||
returncontents: 1
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getEntryCacheKey(dataId, entryId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
entryid: entryId,
|
||||
returncontents: 1,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getEntryCacheKey(dataId, entryId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
component: AddonModDataProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_data_get_entry', params, preSets).then((response) => {
|
||||
response.entry.contents = this.utils.arrayToObject(response.entry.contents, 'fieldid');
|
||||
|
@ -797,27 +735,21 @@ export class AddonModDataProvider {
|
|||
* Get the list of configured fields for the given database.
|
||||
*
|
||||
* @param dataId Data ID.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the fields are retrieved.
|
||||
*/
|
||||
getFields(dataId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getFields(dataId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
databaseid: dataId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getFieldsCacheKey(dataId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
};
|
||||
|
||||
if (forceCache) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
databaseid: dataId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getFieldsCacheKey(dataId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModDataProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_data_get_fields', params, preSets).then((response) => {
|
||||
if (response && response.fields) {
|
||||
|
@ -993,46 +925,45 @@ export class AddonModDataProvider {
|
|||
* Performs search over a database.
|
||||
*
|
||||
* @param dataId The data instance id.
|
||||
* @param groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param search Search text. It will be used if advSearch is not defined.
|
||||
* @param advSearch Advanced search data.
|
||||
* @param sort Sort by this field.
|
||||
* @param order The direction of the sorting.
|
||||
* @param page Page of records to return.
|
||||
* @param perPage Records per page to return. Default on AddonModDataProvider.PER_PAGE.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the action is done.
|
||||
*/
|
||||
searchEntries(dataId: number, groupId: number = 0, search?: string, advSearch?: any, sort?: string, order?: string,
|
||||
page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise<AddonModDataEntries> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
searchEntries(dataId: number, options: AddonModDataSearchEntriesOptions = {}): Promise<AddonModDataEntries> {
|
||||
options.groupId = options.groupId || 0;
|
||||
options.sort = options.sort || 0;
|
||||
options.order || options.order || 'DESC';
|
||||
options.page = options.page || 0;
|
||||
options.perPage = options.perPage || AddonModDataProvider.PER_PAGE;
|
||||
options.readingStrategy = options.readingStrategy || CoreSitesReadingStrategy.PreferNetwork;
|
||||
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
databaseid: dataId,
|
||||
groupid: groupId,
|
||||
returncontents: 1,
|
||||
page: page,
|
||||
perpage: perPage
|
||||
},
|
||||
preSets = {
|
||||
getFromCache: false,
|
||||
saveToCache: true,
|
||||
emergencyCache: true
|
||||
};
|
||||
databaseid: dataId,
|
||||
groupid: options.groupId,
|
||||
returncontents: 1,
|
||||
page: options.page,
|
||||
perpage: options.perPage,
|
||||
};
|
||||
const preSets = {
|
||||
component: AddonModDataProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
if (typeof sort != 'undefined') {
|
||||
params['sort'] = sort;
|
||||
if (typeof options.sort != 'undefined') {
|
||||
params['sort'] = options.sort;
|
||||
}
|
||||
|
||||
if (typeof order !== 'undefined') {
|
||||
params['order'] = order;
|
||||
if (typeof options.order !== 'undefined') {
|
||||
params['order'] = options.order;
|
||||
}
|
||||
|
||||
if (typeof search !== 'undefined') {
|
||||
params['search'] = search;
|
||||
if (typeof options.search !== 'undefined') {
|
||||
params['search'] = options.search;
|
||||
}
|
||||
|
||||
if (typeof advSearch !== 'undefined') {
|
||||
params['advsearch'] = advSearch;
|
||||
if (typeof options.advSearch !== 'undefined') {
|
||||
params['advsearch'] = options.advSearch;
|
||||
}
|
||||
|
||||
return site.read('mod_data_search_entries', params, preSets).then((response) => {
|
||||
|
@ -1045,3 +976,34 @@ export class AddonModDataProvider {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to pass to get access info.
|
||||
*/
|
||||
export type AddonModDataAccessInfoOptions = CoreCourseCommonModWSOptions & {
|
||||
groupId?: number; // Group Id.
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to pass to get entries.
|
||||
*/
|
||||
export type AddonModDataGetEntriesOptions = CoreCourseCommonModWSOptions & {
|
||||
groupId?: number; // Group Id.
|
||||
sort?: number; // Sort the records by this field id, defaults to 0. Reserved ids are:
|
||||
// 0: timeadded
|
||||
// -1: firstname
|
||||
// -2: lastname
|
||||
// -3: approved
|
||||
// -4: timemodified
|
||||
order?: string; // The direction of the sorting: 'ASC' or 'DESC'. Defaults to 'DESC'.
|
||||
page?: number; // Page of records to return. Defaults to 0.
|
||||
perPage?: number; // Records per page to return. Defaults to AddonModDataProvider.PER_PAGE.
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to pass to search entries.
|
||||
*/
|
||||
export type AddonModDataSearchEntriesOptions = AddonModDataGetEntriesOptions & {
|
||||
search?: string; // Search text. It will be used if advSearch is not defined.
|
||||
advSearch?: any; // Advanced search data.
|
||||
};
|
||||
|
|
|
@ -18,12 +18,13 @@ import { CoreEventsProvider } from '@providers/events';
|
|||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||
import { AddonModDataFieldsDelegate } from './fields-delegate';
|
||||
import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline';
|
||||
import { AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields, AddonModDataEntries } from './data';
|
||||
import {
|
||||
AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields, AddonModDataEntries, AddonModDataSearchEntriesOptions
|
||||
} from './data';
|
||||
import { CoreRatingInfo } from '@core/rating/providers/rating';
|
||||
import { CoreRatingOfflineProvider } from '@core/rating/providers/offline';
|
||||
|
||||
|
@ -33,12 +34,19 @@ import { CoreRatingOfflineProvider } from '@core/rating/providers/offline';
|
|||
@Injectable()
|
||||
export class AddonModDataHelperProvider {
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider, protected dataProvider: AddonModDataProvider,
|
||||
private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate,
|
||||
private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider,
|
||||
private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private utils: CoreUtilsProvider,
|
||||
private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider,
|
||||
private ratingOffline: CoreRatingOfflineProvider) {}
|
||||
constructor(
|
||||
protected sitesProvider: CoreSitesProvider,
|
||||
protected dataProvider: AddonModDataProvider,
|
||||
protected translate: TranslateService,
|
||||
protected fieldsDelegate: AddonModDataFieldsDelegate,
|
||||
protected dataOffline: AddonModDataOfflineProvider,
|
||||
protected fileUploaderProvider: CoreFileUploaderProvider,
|
||||
protected textUtils: CoreTextUtilsProvider,
|
||||
protected eventsProvider: CoreEventsProvider,
|
||||
protected domUtils: CoreDomUtilsProvider,
|
||||
protected courseProvider: CoreCourseProvider,
|
||||
protected ratingOffline: CoreRatingOfflineProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the record with the offline actions applied.
|
||||
|
@ -210,33 +218,21 @@ export class AddonModDataHelperProvider {
|
|||
*
|
||||
* @param data Database object.
|
||||
* @param fields The fields that define the contents.
|
||||
* @param groupId Group ID.
|
||||
* @param search Search text. It will be used if advSearch is not defined.
|
||||
* @param advSearch Advanced search data.
|
||||
* @param sort Sort the records by this field id, reserved ids are:
|
||||
* 0: timeadded
|
||||
* -1: firstname
|
||||
* -2: lastname
|
||||
* -3: approved
|
||||
* -4: timemodified.
|
||||
* Empty for using the default database setting.
|
||||
* @param order The direction of the sorting: 'ASC' or 'DESC'.
|
||||
* Empty for using the default database setting.
|
||||
* @param page Page of records to return.
|
||||
* @param perPage Records per page to return. Default on PER_PAGE.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the database is retrieved.
|
||||
*/
|
||||
fetchEntries(data: any, fields: any[], groupId: number = 0, search?: string, advSearch?: any[], sort: string = '0',
|
||||
order: string = 'DESC', page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string):
|
||||
Promise<AddonModDataEntries> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
fetchEntries(data: any, fields: any[], options: AddonModDataSearchEntriesOptions = {}): Promise<AddonModDataEntries> {
|
||||
options.groupId = options.groupId || 0;
|
||||
options.page = options.page || 0;
|
||||
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const offlineActions = {};
|
||||
const result: AddonModDataEntries = {
|
||||
entries: [],
|
||||
totalcount: 0,
|
||||
offlineEntries: []
|
||||
};
|
||||
options.siteId = site.id;
|
||||
|
||||
const offlinePromise = this.dataOffline.getDatabaseEntries(data.id, site.id).then((actions) => {
|
||||
result.hasOfflineActions = !!actions.length;
|
||||
|
@ -248,8 +244,8 @@ export class AddonModDataHelperProvider {
|
|||
offlineActions[action.entryid].push(action);
|
||||
|
||||
// We only display new entries in the first page when not searching.
|
||||
if (action.action == 'add' && page == 0 && !search && !advSearch &&
|
||||
(!action.groupid || !groupId || action.groupid == groupId)) {
|
||||
if (action.action == 'add' && options.page == 0 && !options.search && !options.advSearch &&
|
||||
(!action.groupid || !options.groupId || action.groupid == options.groupId)) {
|
||||
result.offlineEntries.push({
|
||||
id: action.entryid,
|
||||
canmanageentry: true,
|
||||
|
@ -275,16 +271,14 @@ export class AddonModDataHelperProvider {
|
|||
});
|
||||
|
||||
let fetchPromise: Promise<void>;
|
||||
if (search || advSearch) {
|
||||
fetchPromise = this.dataProvider.searchEntries(data.id, groupId, search, advSearch, sort, order, page, perPage,
|
||||
site.id).then((fetchResult) => {
|
||||
if (options.search || options.advSearch) {
|
||||
fetchPromise = this.dataProvider.searchEntries(data.id, options).then((fetchResult) => {
|
||||
result.entries = fetchResult.entries;
|
||||
result.totalcount = fetchResult.totalcount;
|
||||
result.maxcount = fetchResult.maxcount;
|
||||
});
|
||||
} else {
|
||||
fetchPromise = this.dataProvider.getEntries(data.id, groupId, sort, order, page, perPage, false, false, site.id)
|
||||
.then((fetchResult) => {
|
||||
fetchPromise = this.dataProvider.getEntries(data.id, options).then((fetchResult) => {
|
||||
result.entries = fetchResult.entries;
|
||||
result.totalcount = fetchResult.totalcount;
|
||||
});
|
||||
|
@ -324,7 +318,7 @@ export class AddonModDataHelperProvider {
|
|||
|
||||
if (entryId > 0) {
|
||||
// Online entry.
|
||||
promise = this.dataProvider.getEntry(data.id, entryId, false, site.id);
|
||||
promise = this.dataProvider.getEntry(data.id, entryId, {cmId: data.coursemodule, siteId: site.id});
|
||||
} else {
|
||||
// Offline entry or new entry.
|
||||
promise = Promise.resolve({
|
||||
|
@ -644,35 +638,44 @@ export class AddonModDataHelperProvider {
|
|||
* @param courseId Course ID. It not defined, it will be fetched.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
*/
|
||||
showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): void {
|
||||
async showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): Promise<void> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
this.domUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord').then(() => {
|
||||
const modal = this.domUtils.showModalLoading();
|
||||
let modal;
|
||||
try {
|
||||
await this.domUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord');
|
||||
|
||||
return this.getActivityCourseIdIfNotSet(dataId, courseId, siteId).then((courseId) => {
|
||||
return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId);
|
||||
}).catch((message) => {
|
||||
modal = this.domUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
if (entryId > 0) {
|
||||
courseId = await this.getActivityCourseIdIfNotSet(dataId, courseId, siteId);
|
||||
}
|
||||
|
||||
this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId);
|
||||
} catch (message) {
|
||||
this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true);
|
||||
|
||||
return Promise.reject(null);
|
||||
}).then(() => {
|
||||
return this.utils.allPromises([
|
||||
this.dataProvider.invalidateEntryData(dataId, entryId, siteId),
|
||||
this.dataProvider.invalidateEntriesData(dataId, siteId)
|
||||
]).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}).then(() => {
|
||||
this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId, deleted: true}, siteId);
|
||||
modal && modal.dismiss();
|
||||
|
||||
this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000);
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.dataProvider.invalidateEntryData(dataId, entryId, siteId);
|
||||
await this.dataProvider.invalidateEntriesData(dataId, siteId);
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId, deleted: true}, siteId);
|
||||
|
||||
this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000);
|
||||
} catch (error) {
|
||||
// Ignore error, it was already displayed.
|
||||
});
|
||||
}
|
||||
|
||||
modal && modal.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,13 +16,13 @@ import { Injectable } from '@angular/core';
|
|||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreCommentsProvider } from '@core/comments/providers/comments';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course';
|
||||
import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
|
||||
import { AddonModDataProvider, AddonModDataEntry } from './data';
|
||||
import { AddonModDataSyncProvider } from './sync';
|
||||
|
@ -65,16 +65,17 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
|
|||
*
|
||||
* @param dataId Database Id.
|
||||
* @param groups Array of groups in the activity.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID.
|
||||
* @param options Other options.
|
||||
* @return All unique entries.
|
||||
*/
|
||||
protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false,
|
||||
siteId?: string): Promise<AddonModDataEntry[]> {
|
||||
protected getAllUniqueEntries(dataId: number, groups: any[], options: CoreSitesCommonWSOptions = {})
|
||||
: Promise<AddonModDataEntry[]> {
|
||||
|
||||
const promises = groups.map((group) => {
|
||||
return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache,
|
||||
siteId);
|
||||
return this.dataProvider.fetchAllEntries(dataId, {
|
||||
groupId: group.id,
|
||||
...options, // Include all options.
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then((responses) => {
|
||||
|
@ -96,31 +97,30 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
|
|||
* @param module Module to get the files.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param omitFail True to always return even if fails. Default false.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the info fetched.
|
||||
*/
|
||||
protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false,
|
||||
ignoreCache: boolean = false, siteId?: string): Promise<any> {
|
||||
protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean, options: CoreCourseCommonModWSOptions = {})
|
||||
: Promise<any> {
|
||||
let database,
|
||||
groups = [],
|
||||
entries = [],
|
||||
files = [];
|
||||
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
options.cmId = options.cmId || module.id;
|
||||
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.dataProvider.getDatabase(courseId, module.id, siteId, forceCache).then((data) => {
|
||||
return this.dataProvider.getDatabase(courseId, module.id, options).then((data) => {
|
||||
files = this.getIntroFilesFromInstance(module, data);
|
||||
database = data;
|
||||
|
||||
return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => {
|
||||
return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, options.siteId).then((groupInfo) => {
|
||||
if (!groupInfo.groups || groupInfo.groups.length == 0) {
|
||||
groupInfo.groups = [{id: 0}];
|
||||
}
|
||||
groups = groupInfo.groups;
|
||||
|
||||
return this.getAllUniqueEntries(database.id, groups, forceCache, ignoreCache, siteId);
|
||||
return this.getAllUniqueEntries(database.id, groups, options);
|
||||
});
|
||||
}).then((uniqueEntries) => {
|
||||
entries = uniqueEntries;
|
||||
|
@ -229,8 +229,10 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
|
|||
* @return Promise resolved with true if downloadable, resolved with false otherwise.
|
||||
*/
|
||||
isDownloadable(module: any, courseId: number): boolean | Promise<boolean> {
|
||||
return this.dataProvider.getDatabase(courseId, module.id, undefined, true).then((database) => {
|
||||
return this.dataProvider.getDatabaseAccessInformation(database.id).then((accessData) => {
|
||||
return this.dataProvider.getDatabase(courseId, module.id, {
|
||||
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||
}).then((database) => {
|
||||
return this.dataProvider.getDatabaseAccessInformation(database.id, {cmId: module.id}).then((accessData) => {
|
||||
// Check if database is restricted by time.
|
||||
if (!accessData.timeavailable) {
|
||||
const time = this.timeUtils.timestamp();
|
||||
|
@ -281,23 +283,31 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected prefetchDatabase(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||
const options = {
|
||||
cmId: module.id,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
return this.getDatabaseInfoHelper(module, courseId, false, false, true, siteId).then((info) => {
|
||||
return this.getDatabaseInfoHelper(module, courseId, false, options).then((info) => {
|
||||
// Prefetch the database data.
|
||||
const database = info.database,
|
||||
commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(),
|
||||
promises = [];
|
||||
|
||||
promises.push(this.dataProvider.getFields(database.id, false, true, siteId));
|
||||
promises.push(this.dataProvider.getFields(database.id, options));
|
||||
|
||||
promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id));
|
||||
|
||||
info.groups.forEach((group) => {
|
||||
promises.push(this.dataProvider.getDatabaseAccessInformation(database.id, group.id, false, true, siteId));
|
||||
promises.push(this.dataProvider.getDatabaseAccessInformation(database.id, {
|
||||
groupId: group.id,
|
||||
...options, // Include all options.
|
||||
}));
|
||||
});
|
||||
|
||||
info.entries.forEach((entry) => {
|
||||
promises.push(this.dataProvider.getEntry(database.id, entry.id, true, siteId));
|
||||
promises.push(this.dataProvider.getEntry(database.id, entry.id, options));
|
||||
|
||||
if (commentsEnabled && database.comments) {
|
||||
promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id,
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
|
@ -188,7 +188,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
|
|||
courseId = offlineActions[0].courseid;
|
||||
|
||||
// Send the answers.
|
||||
return this.dataProvider.getDatabaseById(courseId, dataId, siteId).then((database) => {
|
||||
return this.dataProvider.getDatabaseById(courseId, dataId, {siteId}).then((database) => {
|
||||
data = database;
|
||||
|
||||
const offlineEntries = {};
|
||||
|
@ -208,7 +208,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
|
|||
}).then(() => {
|
||||
if (result.updated) {
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
return this.dataProvider.invalidateContent(data.cmid, courseId, siteId).catch(() => {
|
||||
return this.dataProvider.invalidateContent(data.coursemodule, courseId, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
|
@ -233,18 +233,23 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
|
|||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected syncEntry(data: any, entryActions: AddonModDataOfflineAction[], result: any, siteId?: string): Promise<any> {
|
||||
let discardError,
|
||||
timePromise,
|
||||
entryId = entryActions[0].entryid,
|
||||
offlineId,
|
||||
deleted = false;
|
||||
let discardError;
|
||||
let timePromise;
|
||||
let entryId = entryActions[0].entryid;
|
||||
let offlineId;
|
||||
let deleted = false;
|
||||
|
||||
const editAction = entryActions.find((action) => action.action == 'add' || action.action == 'edit');
|
||||
const approveAction = entryActions.find((action) => action.action == 'approve' || action.action == 'disapprove');
|
||||
const deleteAction = entryActions.find((action) => action.action == 'delete');
|
||||
const options = {
|
||||
cmId: data.coursemodule,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
if (entryId > 0) {
|
||||
timePromise = this.dataProvider.getEntry(data.id, entryId, true, siteId).then((entry) => {
|
||||
timePromise = this.dataProvider.getEntry(data.id, entryId, options).then((entry) => {
|
||||
return entry.entry.timemodified;
|
||||
}).catch((error) => {
|
||||
if (error && this.utils.isWebServiceError(error)) {
|
||||
|
@ -402,7 +407,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
|
|||
const promises = [];
|
||||
|
||||
results.forEach((result) => {
|
||||
promises.push(this.dataProvider.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, siteId)
|
||||
promises.push(this.dataProvider.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, {siteId})
|
||||
.then((data) => {
|
||||
const promises = [];
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
|
|
|
@ -184,7 +184,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
|
|||
}
|
||||
}).then(() => {
|
||||
// Check if there are answers stored in offline.
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id);
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, {cmId: this.module.id});
|
||||
}).then((accessData) => {
|
||||
this.access = accessData;
|
||||
this.showTabs = (accessData.canviewreports || accessData.canviewanalysis) && !accessData.isempty;
|
||||
|
@ -220,7 +220,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
|
|||
const promises = [];
|
||||
|
||||
if (accessData.cancomplete && accessData.cansubmit && accessData.isopen) {
|
||||
promises.push(this.feedbackProvider.getResumePage(this.feedback.id).then((goPage) => {
|
||||
promises.push(this.feedbackProvider.getResumePage(this.feedback.id, {cmId: this.module.id}).then((goPage) => {
|
||||
this.goPage = goPage > 0 ? goPage : false;
|
||||
}));
|
||||
}
|
||||
|
@ -421,7 +421,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
|
|||
setGroup(groupId: number): Promise<any> {
|
||||
this.group = groupId;
|
||||
|
||||
return this.feedbackProvider.getAnalysis(this.feedback.id, groupId).then((analysis) => {
|
||||
return this.feedbackProvider.getAnalysis(this.feedback.id, {groupId, cmId: this.module.id}).then((analysis) => {
|
||||
this.feedback.completedCount = analysis.completedcount;
|
||||
this.feedback.itemsCount = analysis.itemscount;
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ export class AddonModFeedbackAttemptPage {
|
|||
return this.feedbackProvider.getFeedbackById(this.courseId, this.feedbackId).then((feedback) => {
|
||||
this.feedback = feedback;
|
||||
|
||||
return this.feedbackProvider.getItems(this.feedbackId);
|
||||
return this.feedbackProvider.getItems(this.feedbackId, {cmId: this.feedback.coursemodule});
|
||||
}).then((items) => {
|
||||
// Add responses and format items.
|
||||
this.items = items.items.map((item) => {
|
||||
|
|
|
@ -27,7 +27,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
|
|||
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
|
||||
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
|
||||
/**
|
||||
* Page that displays feedback form.
|
||||
|
@ -141,6 +141,10 @@ export class AddonModFeedbackFormPage implements OnDestroy {
|
|||
*/
|
||||
protected fetchData(): Promise<any> {
|
||||
this.offline = !this.appProvider.isOnline();
|
||||
const options = {
|
||||
cmId: this.module.id,
|
||||
readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
|
||||
};
|
||||
|
||||
return this.feedbackProvider.getFeedback(this.courseId, this.module.id).then((feedbackData) => {
|
||||
this.feedback = feedbackData;
|
||||
|
@ -151,8 +155,7 @@ export class AddonModFeedbackFormPage implements OnDestroy {
|
|||
}).then((accessData) => {
|
||||
if (!this.preview && accessData.cansubmit && !accessData.isempty) {
|
||||
return typeof this.currentPage == 'undefined' ?
|
||||
this.feedbackProvider.getResumePage(this.feedback.id, this.offline, true) :
|
||||
Promise.resolve(this.currentPage);
|
||||
this.feedbackProvider.getResumePage(this.feedback.id, options) : Promise.resolve(this.currentPage);
|
||||
} else {
|
||||
this.preview = true;
|
||||
|
||||
|
@ -162,8 +165,9 @@ export class AddonModFeedbackFormPage implements OnDestroy {
|
|||
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||
// If it fails, go offline.
|
||||
this.offline = true;
|
||||
options.readingStrategy = CoreSitesReadingStrategy.PreferCache;
|
||||
|
||||
return this.feedbackProvider.getResumePage(this.feedback.id, true);
|
||||
return this.feedbackProvider.getResumePage(this.feedback.id, options);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
@ -186,12 +190,18 @@ export class AddonModFeedbackFormPage implements OnDestroy {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected fetchAccessData(): Promise<any> {
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, this.offline, true).catch((error) => {
|
||||
const options = {
|
||||
cmId: this.module.id,
|
||||
readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
|
||||
};
|
||||
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, options).catch((error) => {
|
||||
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||
// If it fails, go offline.
|
||||
this.offline = true;
|
||||
options.readingStrategy = CoreSitesReadingStrategy.PreferCache;
|
||||
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, true);
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, options);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
@ -203,20 +213,25 @@ export class AddonModFeedbackFormPage implements OnDestroy {
|
|||
}
|
||||
|
||||
protected fetchFeedbackPageData(page: number = 0): Promise<void> {
|
||||
const options = {
|
||||
cmId: this.module.id,
|
||||
readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
|
||||
};
|
||||
let promise;
|
||||
this.items = [];
|
||||
|
||||
if (this.preview) {
|
||||
promise = this.feedbackProvider.getItems(this.feedback.id);
|
||||
promise = this.feedbackProvider.getItems(this.feedback.id, {cmId: this.module.id});
|
||||
} else {
|
||||
this.currentPage = page;
|
||||
|
||||
promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, this.offline, true).catch((error) => {
|
||||
promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, options).catch((error) => {
|
||||
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||
// If it fails, go offline.
|
||||
this.offline = true;
|
||||
options.readingStrategy = CoreSitesReadingStrategy.PreferCache;
|
||||
|
||||
return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, true);
|
||||
return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, options);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
@ -262,8 +277,12 @@ export class AddonModFeedbackFormPage implements OnDestroy {
|
|||
return this.feedbackSync.syncFeedback(this.feedback.id).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, goPrevious, formHasErrors,
|
||||
this.courseId).then((response) => {
|
||||
return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, {
|
||||
goPrevious,
|
||||
formHasErrors,
|
||||
courseId: this.courseId,
|
||||
cmId: this.module.id,
|
||||
}).then((response) => {
|
||||
const jumpTo = parseInt(response.jumpto, 10);
|
||||
|
||||
if (response.completed) {
|
||||
|
|
|
@ -111,7 +111,11 @@ export class AddonModFeedbackNonRespondentsPage {
|
|||
this.feedbackLoaded = false;
|
||||
}
|
||||
|
||||
return this.feedbackHelper.getNonRespondents(this.feedbackId, this.selectedGroup, this.page).then((response) => {
|
||||
return this.feedbackHelper.getNonRespondents(this.feedbackId, {
|
||||
groupId: this.selectedGroup,
|
||||
page: this.page,
|
||||
cmId: this.moduleId,
|
||||
}).then((response) => {
|
||||
this.total = response.total;
|
||||
|
||||
if (this.users.length < response.total) {
|
||||
|
|
|
@ -134,7 +134,11 @@ export class AddonModFeedbackRespondentsPage {
|
|||
this.feedbackLoaded = false;
|
||||
}
|
||||
|
||||
return this.feedbackHelper.getResponsesAnalysis(this.feedbackId, this.selectedGroup, this.page).then((responses) => {
|
||||
return this.feedbackHelper.getResponsesAnalysis(this.feedbackId, {
|
||||
groupId: this.selectedGroup,
|
||||
page: this.page,
|
||||
cmId: this.moduleId,
|
||||
}).then((responses) => {
|
||||
this.responses.total = responses.totalattempts;
|
||||
this.anonResponses.total = responses.totalanonattempts;
|
||||
|
||||
|
|
|
@ -14,13 +14,14 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||
import { AddonModFeedbackOfflineProvider } from './offline';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreCourseCommonModWSOptions } from '@core/course/providers/course';
|
||||
|
||||
/**
|
||||
* Service that provides some features for feedbacks.
|
||||
|
@ -35,7 +36,7 @@ export class AddonModFeedbackProvider {
|
|||
static MULTICHOICE_HIDENOSELECT = 'h';
|
||||
static MULTICHOICERATED_VALUE_SEP = '####';
|
||||
|
||||
protected ROOT_CACHE_KEY = this.ROOT_CACHE_KEY + '';
|
||||
protected ROOT_CACHE_KEY = '';
|
||||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
|
||||
|
@ -130,13 +131,11 @@ export class AddonModFeedbackProvider {
|
|||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param items Item to fill the value.
|
||||
* @param offline True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID.
|
||||
* @param options Other options.
|
||||
* @return Resolved with values when done.
|
||||
*/
|
||||
protected fillValues(feedbackId: number, items: any[], offline: boolean, ignoreCache: boolean, siteId: string): Promise<any> {
|
||||
return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId).then((valuesArray) => {
|
||||
protected fillValues(feedbackId: number, items: any[], options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.getCurrentValues(feedbackId, options).then((valuesArray) => {
|
||||
const values = {};
|
||||
|
||||
valuesArray.forEach((value) => {
|
||||
|
@ -152,7 +151,7 @@ export class AddonModFeedbackProvider {
|
|||
// Ignore errors.
|
||||
}).then(() => {
|
||||
// Merge with offline data.
|
||||
return this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).then((offlineValuesArray) => {
|
||||
return this.feedbackOffline.getFeedbackResponses(feedbackId, options.siteId).then((offlineValuesArray) => {
|
||||
const offlineValues = {};
|
||||
|
||||
// Merge all values into one array.
|
||||
|
@ -203,24 +202,22 @@ export class AddonModFeedbackProvider {
|
|||
* Returns all the feedback non respondents users.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @param previous Only for recurrent use. Object with the previous fetched info.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getAllNonRespondents(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any)
|
||||
: Promise<any> {
|
||||
getAllNonRespondents(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise<any> {
|
||||
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
if (typeof previous == 'undefined') {
|
||||
previous = {
|
||||
page: 0,
|
||||
users: []
|
||||
};
|
||||
}
|
||||
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
||||
previous = previous || {
|
||||
page: 0,
|
||||
users: []
|
||||
};
|
||||
|
||||
return this.getNonRespondents(feedbackId, groupId, previous.page, ignoreCache, siteId).then((response) => {
|
||||
return this.getNonRespondents(feedbackId, {
|
||||
page: previous.page,
|
||||
...options, // Include all options.
|
||||
}).then((response) => {
|
||||
if (previous.users.length < response.total) {
|
||||
previous.users = previous.users.concat(response.users);
|
||||
}
|
||||
|
@ -229,7 +226,7 @@ export class AddonModFeedbackProvider {
|
|||
// Can load more.
|
||||
previous.page++;
|
||||
|
||||
return this.getAllNonRespondents(feedbackId, groupId, ignoreCache, siteId, previous);
|
||||
return this.getAllNonRespondents(feedbackId, options, previous);
|
||||
}
|
||||
previous.total = response.total;
|
||||
|
||||
|
@ -241,25 +238,23 @@ export class AddonModFeedbackProvider {
|
|||
* Returns all the feedback user responses.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @param previous Only for recurrent use. Object with the previous fetched info.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getAllResponsesAnalysis(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any)
|
||||
: Promise<any> {
|
||||
getAllResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise<any> {
|
||||
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
if (typeof previous == 'undefined') {
|
||||
previous = {
|
||||
page: 0,
|
||||
attempts: [],
|
||||
anonattempts: []
|
||||
};
|
||||
}
|
||||
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
||||
previous = previous || {
|
||||
page: 0,
|
||||
attempts: [],
|
||||
anonattempts: []
|
||||
};
|
||||
|
||||
return this.getResponsesAnalysis(feedbackId, groupId, previous.page, ignoreCache, siteId).then((responses) => {
|
||||
return this.getResponsesAnalysis(feedbackId, {
|
||||
page: previous.page,
|
||||
...options, // Include all options.
|
||||
}).then((responses) => {
|
||||
if (previous.anonattempts.length < responses.totalanonattempts) {
|
||||
previous.anonattempts = previous.anonattempts.concat(responses.anonattempts);
|
||||
}
|
||||
|
@ -272,7 +267,7 @@ export class AddonModFeedbackProvider {
|
|||
// Can load more.
|
||||
previous.page++;
|
||||
|
||||
return this.getAllResponsesAnalysis(feedbackId, groupId, ignoreCache, siteId, previous);
|
||||
return this.getAllResponsesAnalysis(feedbackId, options, previous);
|
||||
}
|
||||
|
||||
previous.totalattempts = responses.totalattempts;
|
||||
|
@ -286,27 +281,23 @@ export class AddonModFeedbackProvider {
|
|||
* Get analysis information for a given feedback.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param groupId Group ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
getAnalysis(feedbackId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getAnalysisDataCacheKey(feedbackId, groupId)
|
||||
};
|
||||
feedbackid: feedbackId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getAnalysisDataCacheKey(feedbackId, options.groupId),
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
if (groupId) {
|
||||
params['groupid'] = groupId;
|
||||
}
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
if (options.groupId) {
|
||||
params['groupid'] = options.groupId;
|
||||
}
|
||||
|
||||
return site.read('mod_feedback_get_analysis', params, preSets);
|
||||
|
@ -339,22 +330,23 @@ export class AddonModFeedbackProvider {
|
|||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param attemptId Attempt id to find.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @param previous Only for recurrent use. Object with the previous fetched info.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getAttempt(feedbackId: number, attemptId: number, ignoreCache?: boolean, siteId?: string, previous?: any): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
if (typeof previous == 'undefined') {
|
||||
previous = {
|
||||
page: 0,
|
||||
attemptsLoaded: 0,
|
||||
anonAttemptsLoaded: 0
|
||||
};
|
||||
}
|
||||
getAttempt(feedbackId: number, attemptId: number, options: CoreCourseCommonModWSOptions = {}, previous?: any): Promise<any> {
|
||||
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
||||
previous = previous || {
|
||||
page: 0,
|
||||
attemptsLoaded: 0,
|
||||
anonAttemptsLoaded: 0
|
||||
};
|
||||
|
||||
return this.getResponsesAnalysis(feedbackId, 0, previous.page, ignoreCache, siteId).then((responses) => {
|
||||
return this.getResponsesAnalysis(feedbackId, {
|
||||
page: previous.page,
|
||||
groupId: 0,
|
||||
...options, // Include all options.
|
||||
}).then((responses) => {
|
||||
let attempt;
|
||||
|
||||
attempt = responses.attempts.find((attempt) => {
|
||||
|
@ -385,7 +377,7 @@ export class AddonModFeedbackProvider {
|
|||
// Can load more. Check there.
|
||||
previous.page++;
|
||||
|
||||
return this.getAttempt(feedbackId, attemptId, ignoreCache, siteId, previous);
|
||||
return this.getAttempt(feedbackId, attemptId, options, previous);
|
||||
}
|
||||
|
||||
// Not found and all loaded. Reject.
|
||||
|
@ -407,23 +399,20 @@ export class AddonModFeedbackProvider {
|
|||
* Returns the temporary completion timemodified for the current user.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getCurrentCompletedTimeModified(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getCurrentCompletedTimeModified(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
feedbackid: feedbackId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId),
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_get_current_completed_tmp', params, preSets).then((response) => {
|
||||
if (response && typeof response.feedback != 'undefined' && typeof response.feedback.timemodified != 'undefined') {
|
||||
|
@ -452,26 +441,20 @@ export class AddonModFeedbackProvider {
|
|||
* Returns the temporary responses or responses of the last submission for the current user.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param offline True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param ignoreCache True if it should ignore cached data (it always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getCurrentValues(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getCurrentValues(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getCurrentValuesDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
if (offline) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
feedbackid: feedbackId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getCurrentValuesDataCacheKey(feedbackId),
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_get_unfinished_responses', params, preSets).then((response) => {
|
||||
if (!response || typeof response.responses == 'undefined') {
|
||||
|
@ -508,27 +491,20 @@ export class AddonModFeedbackProvider {
|
|||
* Get access information for a given feedback.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param offline True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param ignoreCache True if it should ignore cached data (it always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
getFeedbackAccessInformation(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string):
|
||||
Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getFeedbackAccessInformation(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
if (offline) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
feedbackid: feedbackId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId),
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_get_feedback_access_information', params, preSets);
|
||||
});
|
||||
|
@ -570,29 +546,22 @@ export class AddonModFeedbackProvider {
|
|||
* @param courseId Course ID.
|
||||
* @param key Name of the property to check.
|
||||
* @param value Value to search.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean,
|
||||
ignoreCache?: boolean): Promise<any> {
|
||||
protected getFeedbackDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {})
|
||||
: Promise<any> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getFeedbackCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
};
|
||||
|
||||
if (forceCache) {
|
||||
preSets.omitExpires = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
courseids: [courseId],
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getFeedbackCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_get_feedbacks_by_courses', params, preSets).then((response) => {
|
||||
if (response && response.feedbacks) {
|
||||
|
@ -614,13 +583,11 @@ export class AddonModFeedbackProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param cmId Course module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise<any> {
|
||||
return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache, ignoreCache);
|
||||
getFeedback(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
||||
return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -628,37 +595,32 @@ export class AddonModFeedbackProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param id Feedback ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param forceCache True to always get the value from cache, false otherwise. Default false.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise<any> {
|
||||
return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache, ignoreCache);
|
||||
getFeedbackById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
||||
return this.getFeedbackDataByKey(courseId, 'id', id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the items (questions) in the given feedback.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getItems(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getItems(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getItemsDataCacheKey(feedbackId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
feedbackid: feedbackId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getItemsDataCacheKey(feedbackId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_get_items', params, preSets);
|
||||
});
|
||||
|
@ -678,29 +640,25 @@ export class AddonModFeedbackProvider {
|
|||
* Retrieves a list of students who didn't submit the feedback.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param page The page of records to return.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, ignoreCache?: boolean, siteId?: string)
|
||||
: Promise<any> {
|
||||
getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> {
|
||||
options.groupId = options.groupId || 0;
|
||||
options.page = options.page || 0;
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId,
|
||||
groupid: groupId,
|
||||
page: page
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, groupId)
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
feedbackid: feedbackId,
|
||||
groupid: options.groupId,
|
||||
page: options.page,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, options.groupId),
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_get_non_respondents', params, preSets);
|
||||
});
|
||||
|
@ -751,25 +709,22 @@ export class AddonModFeedbackProvider {
|
|||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param page The page to get.
|
||||
* @param offline True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getPageItemsWithValues(feedbackId: number, page: number, offline: boolean = false, ignoreCache: boolean = false,
|
||||
siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
getPageItemsWithValues(feedbackId: number, page: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.getPageItems(feedbackId, page, siteId).then((response) => {
|
||||
return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => {
|
||||
return this.getPageItems(feedbackId, page, options.siteId).then((response) => {
|
||||
return this.fillValues(feedbackId, response.items, options).then((items) => {
|
||||
response.items = items;
|
||||
|
||||
return response;
|
||||
});
|
||||
}).catch(() => {
|
||||
// If getPageItems fail we should calculate it using getItems.
|
||||
return this.getItems(feedbackId, false, siteId).then((response) => {
|
||||
return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => {
|
||||
return this.getItems(feedbackId, options).then((response) => {
|
||||
return this.fillValues(feedbackId, response.items, options).then((items) => {
|
||||
// Separate items by pages.
|
||||
let currentPage = 0;
|
||||
const previousPageItems = [];
|
||||
|
@ -819,11 +774,17 @@ export class AddonModFeedbackProvider {
|
|||
* @param feedbackId Feedback ID.
|
||||
* @param page Page where we want to jump.
|
||||
* @param changePage If page change is forward (1) or backward (-1).
|
||||
* @param siteId Site ID.
|
||||
* @param options Other options.
|
||||
* @return Page number where to jump. Or false if completed or first page.
|
||||
*/
|
||||
protected getPageJumpTo(feedbackId: number, page: number, changePage: number, siteId: string): Promise<number | false> {
|
||||
return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => {
|
||||
protected getPageJumpTo(feedbackId: number, page: number, changePage: number, options: {cmId?: number, siteId?: string})
|
||||
: Promise<number | false> {
|
||||
|
||||
return this.getPageItemsWithValues(feedbackId, page, {
|
||||
cmId: options.cmId,
|
||||
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||
siteId: options.siteId,
|
||||
}).then((resp) => {
|
||||
// The page we are going has items.
|
||||
if (resp.items.length > 0) {
|
||||
return page;
|
||||
|
@ -831,7 +792,7 @@ export class AddonModFeedbackProvider {
|
|||
|
||||
// Check we can jump futher.
|
||||
if ((changePage == 1 && resp.hasnextpage) || (changePage == -1 && resp.hasprevpage)) {
|
||||
return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId);
|
||||
return this.getPageJumpTo(feedbackId, page + changePage, changePage, options);
|
||||
}
|
||||
|
||||
// Completed or first page.
|
||||
|
@ -843,27 +804,25 @@ export class AddonModFeedbackProvider {
|
|||
* Returns the feedback user responses.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param page The page of records to return.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getResponsesAnalysis(feedbackId: number, groupId: number, page: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId,
|
||||
groupid: groupId || 0,
|
||||
page: page || 0
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, groupId)
|
||||
};
|
||||
getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> {
|
||||
options.groupId = options.groupId || 0;
|
||||
options.page = options.page || 0;
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId,
|
||||
groupid: options.groupId,
|
||||
page: options.page,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, options.groupId),
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_get_responses_analysis', params, preSets);
|
||||
});
|
||||
|
@ -894,26 +853,20 @@ export class AddonModFeedbackProvider {
|
|||
* Gets the resume page information.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param offline True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param ignoreCache True if it should ignore cached data (it always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getResumePage(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getResumePage(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getResumePageDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
if (offline) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
feedbackid: feedbackId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getResumePageDataCacheKey(feedbackId),
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_launch_feedback', params, preSets).then((response) => {
|
||||
if (response && typeof response.gopage != 'undefined') {
|
||||
|
@ -964,7 +917,7 @@ export class AddonModFeedbackProvider {
|
|||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
* To invalidate files, use AddonFeedbackProvider#invalidateFiles.
|
||||
* To invalidate files, use AddonModFeedbackProvider#invalidateFiles.
|
||||
*
|
||||
* @param moduleId The module ID.
|
||||
* @param courseId Course ID of the module.
|
||||
|
@ -976,7 +929,7 @@ export class AddonModFeedbackProvider {
|
|||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.getFeedback(courseId, moduleId, siteId).then((feedback) => {
|
||||
promises.push(this.getFeedback(courseId, moduleId, {siteId}).then((feedback) => {
|
||||
const ps = [];
|
||||
|
||||
// Do not invalidate module data before getting module info, we need it!
|
||||
|
@ -1086,23 +1039,20 @@ export class AddonModFeedbackProvider {
|
|||
* Returns if feedback has been completed
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
isCompleted(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise<boolean> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
isCompleted(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise<boolean> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getCompletedDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
feedbackid: feedbackId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getCompletedDataCacheKey(feedbackId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModFeedbackProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return this.utils.promiseWorks(site.read('mod_feedback_get_last_completed', params, preSets));
|
||||
});
|
||||
|
@ -1147,19 +1097,15 @@ export class AddonModFeedbackProvider {
|
|||
* @param feedbackId Feedback ID.
|
||||
* @param page The page being processed.
|
||||
* @param responses The data to be processed the key is the field name (usually type[index]_id).
|
||||
* @param goPrevious Whether we want to jump to previous page.
|
||||
* @param formHasErrors Whether the form we sent has required but empty fields (only used in offline).
|
||||
* @param courseId Course ID the feedback belongs to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
processPage(feedbackId: number, page: number, responses: any, goPrevious: boolean, formHasErrors: boolean, courseId: number,
|
||||
siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
processPage(feedbackId: number, page: number, responses: any, options: AddonModFeedbackProcessPageOptions = {}): Promise<any> {
|
||||
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Convenience function to store a message to be synchronized later.
|
||||
const storeOffline = (): Promise<any> => {
|
||||
return this.feedbackOffline.saveResponses(feedbackId, page, responses, courseId, siteId).then(() => {
|
||||
return this.feedbackOffline.saveResponses(feedbackId, page, responses, options.courseId, options.siteId).then(() => {
|
||||
// Simulate process_page response.
|
||||
const response = {
|
||||
jumpto: page,
|
||||
|
@ -1168,11 +1114,11 @@ export class AddonModFeedbackProvider {
|
|||
};
|
||||
let changePage = 0;
|
||||
|
||||
if (goPrevious) {
|
||||
if (options.goPrevious) {
|
||||
if (page > 0) {
|
||||
changePage = -1;
|
||||
}
|
||||
} else if (!formHasErrors) {
|
||||
} else if (!options.formHasErrors) {
|
||||
// We can only go next if it has no errors.
|
||||
changePage = 1;
|
||||
}
|
||||
|
@ -1181,7 +1127,11 @@ export class AddonModFeedbackProvider {
|
|||
return response;
|
||||
}
|
||||
|
||||
return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => {
|
||||
return this.getPageItemsWithValues(feedbackId, page, {
|
||||
cmId: options.cmId,
|
||||
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||
siteId: options.siteId,
|
||||
}).then((resp) => {
|
||||
// Check completion.
|
||||
if (changePage == 1 && !resp.hasnextpage) {
|
||||
response.completed = true;
|
||||
|
@ -1189,7 +1139,7 @@ export class AddonModFeedbackProvider {
|
|||
return response;
|
||||
}
|
||||
|
||||
return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId).then((loadPage) => {
|
||||
return this.getPageJumpTo(feedbackId, page + changePage, changePage, options).then((loadPage) => {
|
||||
if (loadPage === false) {
|
||||
// Completed or first page.
|
||||
if (changePage == -1) {
|
||||
|
@ -1215,8 +1165,8 @@ export class AddonModFeedbackProvider {
|
|||
}
|
||||
|
||||
// If there's already a response to be sent to the server, discard it first.
|
||||
return this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, siteId).then(() => {
|
||||
return this.processPageOnline(feedbackId, page, responses, goPrevious, siteId).catch((error) => {
|
||||
return this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, options.siteId).then(() => {
|
||||
return this.processPageOnline(feedbackId, page, responses, options.goPrevious, options.siteId).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
return Promise.reject(error);
|
||||
|
@ -1252,7 +1202,7 @@ export class AddonModFeedbackProvider {
|
|||
}).then((response) => {
|
||||
// Invalidate and update current values because they will change.
|
||||
return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => {
|
||||
return this.getCurrentValues(feedbackId, false, false, site.getId());
|
||||
return this.getCurrentValues(feedbackId, {siteId: site.getId()});
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
|
@ -1262,3 +1212,28 @@ export class AddonModFeedbackProvider {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common options with a group ID.
|
||||
*/
|
||||
export type AddonModFeedbackGroupOptions = CoreCourseCommonModWSOptions & {
|
||||
groupId?: number; // Group id, 0 means that the function will determine the user group. Defaults to 0.
|
||||
};
|
||||
|
||||
/**
|
||||
* Common options with a group ID and page.
|
||||
*/
|
||||
export type AddonModFeedbackGroupPaginatedOptions = AddonModFeedbackGroupOptions & {
|
||||
page?: number; // The page of records to return. The page of records to return.
|
||||
};
|
||||
|
||||
/**
|
||||
* Common options with a group ID and page.
|
||||
*/
|
||||
export type AddonModFeedbackProcessPageOptions = {
|
||||
goPrevious?: boolean; // Whether we want to jump to previous page.
|
||||
formHasErrors?: boolean; // Whether the form we sent has required but empty fields (only used in offline).
|
||||
cmId?: number; // Module ID.
|
||||
courseId?: number; // Course ID the feedback belongs to.
|
||||
siteId?: string; // Site ID. If not defined, current site.;
|
||||
};
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NavController, ViewController } from 'ionic-angular';
|
||||
import { AddonModFeedbackProvider } from './feedback';
|
||||
import { AddonModFeedbackProvider, AddonModFeedbackGroupPaginatedOptions } from './feedback';
|
||||
import { CoreUserProvider } from '@core/user/providers/user';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
|
@ -86,12 +86,11 @@ export class AddonModFeedbackHelperProvider {
|
|||
* Retrieves a list of students who didn't submit the feedback with extra info.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param page The page of records to return.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getNonRespondents(feedbackId: number, groupId: number, page: number): Promise<any> {
|
||||
return this.feedbackProvider.getNonRespondents(feedbackId, groupId, page).then((responses) => {
|
||||
getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> {
|
||||
return this.feedbackProvider.getNonRespondents(feedbackId, options).then((responses) => {
|
||||
return this.addImageProfileToAttempts(responses.users).then((users) => {
|
||||
responses.users = users;
|
||||
|
||||
|
@ -186,12 +185,11 @@ export class AddonModFeedbackHelperProvider {
|
|||
* Returns the feedback user responses with extra info.
|
||||
*
|
||||
* @param feedbackId Feedback ID.
|
||||
* @param groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param page The page of records to return.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getResponsesAnalysis(feedbackId: number, groupId: number, page: number): Promise<any> {
|
||||
return this.feedbackProvider.getResponsesAnalysis(feedbackId, groupId, page).then((responses) => {
|
||||
getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise<any> {
|
||||
return this.feedbackProvider.getResponsesAnalysis(feedbackId, options).then((responses) => {
|
||||
return this.addImageProfileToAttempts(responses.attempts).then((attempts) => {
|
||||
responses.attempts = attempts;
|
||||
|
||||
|
@ -227,7 +225,11 @@ export class AddonModFeedbackHelperProvider {
|
|||
return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackRespondentsPage', stateParams, siteId);
|
||||
}
|
||||
|
||||
return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, true, siteId).then((attempt) => {
|
||||
return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, {
|
||||
cmId: moduleId,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
}).then((attempt) => {
|
||||
stateParams = {
|
||||
moduleId: module.id,
|
||||
attempt: attempt,
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core';
|
|||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
|
@ -143,7 +143,9 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH
|
|||
* @return Promise resolved with true if downloadable, resolved with false otherwise.
|
||||
*/
|
||||
isDownloadable(module: any, courseId: number): boolean | Promise<boolean> {
|
||||
return this.feedbackProvider.getFeedback(courseId, module.id, undefined, true).then((feedback) => {
|
||||
return this.feedbackProvider.getFeedback(courseId, module.id, {
|
||||
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||
}).then((feedback) => {
|
||||
const now = this.timeUtils.timestamp();
|
||||
|
||||
// Check time first if available.
|
||||
|
@ -154,7 +156,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH
|
|||
return false;
|
||||
}
|
||||
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(feedback.id).then((accessData) => {
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, {cmId: module.id}).then((accessData) => {
|
||||
return accessData.isopen;
|
||||
});
|
||||
});
|
||||
|
@ -192,15 +194,24 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected prefetchFeedback(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||
const commonOptions = {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
const modOptions = {
|
||||
cmId: module.id,
|
||||
...commonOptions, // Include all common options.
|
||||
};
|
||||
|
||||
// Prefetch the feedback data.
|
||||
return this.feedbackProvider.getFeedback(courseId, module.id, siteId, false, true).then((feedback) => {
|
||||
return this.feedbackProvider.getFeedback(courseId, module.id, commonOptions).then((feedback) => {
|
||||
let files = (feedback.pageaftersubmitfiles || []).concat(this.getIntroFilesFromInstance(module, feedback));
|
||||
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, false, true, siteId).then((accessData) => {
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, modOptions).then((accessData) => {
|
||||
const p2 = [];
|
||||
if (accessData.canedititems || accessData.canviewreports) {
|
||||
// Get all groups analysis.
|
||||
p2.push(this.feedbackProvider.getAnalysis(feedback.id, undefined, true, siteId));
|
||||
p2.push(this.feedbackProvider.getAnalysis(feedback.id, modOptions));
|
||||
p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId, true)
|
||||
.then((groupInfo) => {
|
||||
const p3 = [];
|
||||
|
@ -209,11 +220,16 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH
|
|||
groupInfo.groups = [{id: 0}];
|
||||
}
|
||||
groupInfo.groups.forEach((group) => {
|
||||
p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, true, siteId));
|
||||
p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, true, siteId));
|
||||
const groupOptions = {
|
||||
groupId: group.id,
|
||||
...modOptions, // Include all mod options.
|
||||
};
|
||||
|
||||
p3.push(this.feedbackProvider.getAnalysis(feedback.id, groupOptions));
|
||||
p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, groupOptions));
|
||||
|
||||
if (!accessData.isanonymous) {
|
||||
p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, group.id, true, siteId));
|
||||
p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, groupOptions));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -221,7 +237,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH
|
|||
}));
|
||||
}
|
||||
|
||||
p2.push(this.feedbackProvider.getItems(feedback.id, true, siteId).then((response) => {
|
||||
p2.push(this.feedbackProvider.getItems(feedback.id, commonOptions).then((response) => {
|
||||
response.items.forEach((item) => {
|
||||
files = files.concat(item.itemfiles);
|
||||
});
|
||||
|
@ -234,8 +250,8 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH
|
|||
p2.push(this.feedbackProvider.processPageOnline(feedback.id, 0, {}, undefined, siteId).finally(() => {
|
||||
const p4 = [];
|
||||
|
||||
p4.push(this.feedbackProvider.getCurrentValues(feedback.id, false, true, siteId));
|
||||
p4.push(this.feedbackProvider.getResumePage(feedback.id, false, true, siteId));
|
||||
p4.push(this.feedbackProvider.getCurrentValues(feedback.id, modOptions));
|
||||
p4.push(this.feedbackProvider.getResumePage(feedback.id, modOptions));
|
||||
|
||||
return Promise.all(p4);
|
||||
}));
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
|
@ -192,12 +192,12 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv
|
|||
|
||||
courseId = responses[0].courseid;
|
||||
|
||||
return this.feedbackProvider.getFeedbackById(courseId, feedbackId, siteId).then((feedbackData) => {
|
||||
return this.feedbackProvider.getFeedbackById(courseId, feedbackId, {siteId}).then((feedbackData) => {
|
||||
feedback = feedbackData;
|
||||
|
||||
if (!feedback.multiple_submit) {
|
||||
// If it does not admit multiple submits, check if it is completed to know if we can submit.
|
||||
return this.feedbackProvider.isCompleted(feedbackId);
|
||||
return this.feedbackProvider.isCompleted(feedbackId, {cmId: feedback.coursemodule, siteId});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -220,7 +220,10 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv
|
|||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, true, siteId).then((timemodified) => {
|
||||
return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
}).then((timemodified) => {
|
||||
// Sort by page.
|
||||
responses.sort((a, b) => {
|
||||
return a.page - b.page;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<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="!subfolder" [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($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||
|
@ -41,11 +41,11 @@ export class AddonModFolderProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param cmId Course module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the book is retrieved.
|
||||
*/
|
||||
getFolder(courseId: number, cmId: number, siteId?: string): Promise<AddonModFolderFolder> {
|
||||
return this.getFolderByKey(courseId, 'coursemodule', cmId, siteId);
|
||||
getFolder(courseId: number, cmId: number, options?: CoreSitesCommonWSOptions): Promise<AddonModFolderFolder> {
|
||||
return this.getFolderByKey(courseId, 'coursemodule', cmId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,18 +54,21 @@ export class AddonModFolderProvider {
|
|||
* @param courseId Course ID.
|
||||
* @param key Name of the property to check.
|
||||
* @param value Value to search.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the book is retrieved.
|
||||
*/
|
||||
protected getFolderByKey(courseId: number, key: string, value: any, siteId?: string): Promise<AddonModFolderFolder> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
protected getFolderByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {})
|
||||
: Promise<AddonModFolderFolder> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getFolderCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
};
|
||||
courseids: [courseId],
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getFolderCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModFolderProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_folder_get_folders_by_courses', params, preSets)
|
||||
.then((response: AddonModFolderGetFoldersByCoursesResult): any => {
|
||||
|
|
|
@ -49,7 +49,7 @@ export class AddonForumDiscussionOptionsMenuComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
if (this.forumProvider.isSetPinStateAvailableForSite()) {
|
||||
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
||||
this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => {
|
||||
this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => {
|
||||
this.canPin = !!response.canpindiscussions;
|
||||
}).catch(() => {
|
||||
this.canPin = false;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<core-context-menu-item *ngIf="loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700" [content]="'addon.mod_forum.refreshdiscussions' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && (hasOffline || hasOfflineRatings) && 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($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="sortingAvailable" [priority]="300" [content]="'core.sort' | translate" (action)="showSortOrderSelector($event)" iconAction="fa-sort"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
|
|
@ -133,7 +133,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
}
|
||||
|
||||
if (typeof data.deleted != 'undefined' && data.deleted) {
|
||||
if (data.post.parent == 0 && this.splitviewCtrl && this.splitviewCtrl.isOn()) {
|
||||
if (data.post.parentid == 0 && this.splitviewCtrl && this.splitviewCtrl.isOn()) {
|
||||
// Discussion deleted, clear details page.
|
||||
this.splitviewCtrl.emptyDetails();
|
||||
}
|
||||
|
@ -250,7 +250,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
promises.push(this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => {
|
||||
this.usesGroups = (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS);
|
||||
}));
|
||||
promises.push(this.forumProvider.getAccessInformation(this.forum.id).then((accessInfo) => {
|
||||
promises.push(this.forumProvider.getAccessInformation(this.forum.id, {cmId: this.module.id}).then((accessInfo) => {
|
||||
// Disallow adding discussions if cut-off date is reached and the user has not the capability to override it.
|
||||
// Just in case the forum was fetched from WS when the cut-off date was not reached but it is now.
|
||||
const cutoffDateReached = this.forumHelper.isCutoffDateReached(this.forum) && !accessInfo.cancanoverridecutoff;
|
||||
|
@ -259,7 +259,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
|
||||
if (this.forumProvider.isSetPinStateAvailableForSite()) {
|
||||
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
||||
promises.push(this.forumProvider.canAddDiscussionToAll(this.forum.id).then((response) => {
|
||||
promises.push(this.forumProvider.canAddDiscussionToAll(this.forum.id, {cmId: this.module.id}).then((response) => {
|
||||
this.canPin = !!response.canpindiscussions;
|
||||
}).catch(() => {
|
||||
this.canPin = false;
|
||||
|
@ -354,7 +354,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
this.page = 0;
|
||||
}
|
||||
|
||||
return this.forumProvider.getDiscussions(this.forum.id, this.selectedSortOrder.value, this.page).then((response) => {
|
||||
return this.forumProvider.getDiscussions(this.forum.id, {
|
||||
cmId: this.forum.cmid,
|
||||
sortOrder: this.selectedSortOrder.value,
|
||||
page: this.page,
|
||||
}).then((response) => {
|
||||
let promise;
|
||||
if (this.usesGroups) {
|
||||
promise = this.forumProvider.formatDiscussionsGroups(this.forum.cmid, response.discussions);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
<ion-item text-wrap (click)="editPost()" *ngIf="canEdit">
|
||||
<ion-item text-wrap (click)="editPost()" *ngIf="offlinePost || (canEdit && isOnline)">
|
||||
<ion-icon name="create" item-start></ion-icon>
|
||||
<h2>{{ 'addon.mod_forum.edit' | translate }}</h2>
|
||||
</ion-item>
|
||||
<ion-item text-wrap (click)="deletePost()" *ngIf="canDelete">
|
||||
<ion-item text-wrap (click)="deletePost()" *ngIf="offlinePost || (canDelete && isOnline)">
|
||||
<ion-icon name="trash" item-start></ion-icon>
|
||||
<h2 *ngIf="post.id">{{ 'addon.mod_forum.delete' | translate }}</h2>
|
||||
<h2 *ngIf="!post.id">{{ 'core.discard' | translate }}</h2>
|
||||
<h2 *ngIf="!offlinePost">{{ 'addon.mod_forum.delete' | translate }}</h2>
|
||||
<h2 *ngIf="offlinePost">{{ 'core.discard' | translate }}</h2>
|
||||
</ion-item>
|
||||
<ion-item text-wrap (click)="dismiss()" *ngIf="wordCount">
|
||||
<h2>{{ 'core.numwords' | translate: {'$a': wordCount} }}</h2>
|
||||
|
|
|
@ -12,12 +12,14 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, NgZone } from '@angular/core';
|
||||
import { NavParams, ViewController } from 'ionic-angular';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { AddonModForumProvider } from '../../providers/forum';
|
||||
import { CoreApp } from '@providers/app';
|
||||
import { Network } from '@ionic-native/network';
|
||||
|
||||
/**
|
||||
* This component is meant to display a popover with the post options.
|
||||
|
@ -34,43 +36,70 @@ export class AddonForumPostOptionsMenuComponent implements OnInit {
|
|||
canDelete = false;
|
||||
loaded = false;
|
||||
url: string;
|
||||
isOnline: boolean;
|
||||
offlinePost: boolean;
|
||||
|
||||
protected cmId: number;
|
||||
protected onlineObserver: any;
|
||||
|
||||
constructor(navParams: NavParams,
|
||||
network: Network,
|
||||
zone: NgZone,
|
||||
protected viewCtrl: ViewController,
|
||||
protected domUtils: CoreDomUtilsProvider,
|
||||
protected forumProvider: AddonModForumProvider,
|
||||
protected sitesProvider: CoreSitesProvider) {
|
||||
this.post = navParams.get('post');
|
||||
this.forumId = navParams.get('forumId');
|
||||
this.cmId = navParams.get('cmId');
|
||||
|
||||
this.isOnline = CoreApp.instance.isOnline();
|
||||
this.onlineObserver = network.onchange().subscribe(() => {
|
||||
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||
zone.run(() => {
|
||||
this.isOnline = CoreApp.instance.isOnline();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (this.forumId) {
|
||||
if (this.post.id) {
|
||||
const site: CoreSite = this.sitesProvider.getCurrentSite();
|
||||
this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussion}, 'p' + this.post.id);
|
||||
|
||||
this.forumProvider.getDiscussionPost(this.forumId, this.post.discussion, this.post.id, true).then((post) => {
|
||||
this.canDelete = post.capabilities.delete && this.forumProvider.isDeletePostAvailable();
|
||||
this.canEdit = post.capabilities.edit && this.forumProvider.isUpdatePostAvailable();
|
||||
this.wordCount = post.wordcount;
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.');
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
} else {
|
||||
// Offline post, you can edit or discard the post.
|
||||
this.canEdit = true;
|
||||
this.canDelete = true;
|
||||
this.loaded = true;
|
||||
}
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (this.post.id > 0) {
|
||||
const site: CoreSite = this.sitesProvider.getCurrentSite();
|
||||
this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussionid}, 'p' + this.post.id);
|
||||
this.offlinePost = false;
|
||||
} else {
|
||||
// Offline post, you can edit or discard the post.
|
||||
this.loaded = true;
|
||||
this.offlinePost = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.post.capabilities.delete == 'undefined') {
|
||||
if (this.forumId) {
|
||||
try {
|
||||
this.post =
|
||||
await this.forumProvider.getDiscussionPost(this.forumId, this.post.discussionid, this.post.id, {
|
||||
cmId: this.cmId,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
});
|
||||
} catch (error) {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.');
|
||||
}
|
||||
} else {
|
||||
this.loaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.canDelete = this.post.capabilities.delete && this.forumProvider.isDeletePostAvailable();
|
||||
this.canEdit = this.post.capabilities.edit && this.forumProvider.isUpdatePostAvailable();
|
||||
this.wordCount = this.post.haswordcount && this.post.wordcount;
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,7 +113,7 @@ export class AddonForumPostOptionsMenuComponent implements OnInit {
|
|||
* Delete a post.
|
||||
*/
|
||||
deletePost(): void {
|
||||
if (this.post.id) {
|
||||
if (!this.offlinePost) {
|
||||
this.viewCtrl.dismiss({action: 'delete'});
|
||||
} else {
|
||||
this.viewCtrl.dismiss({action: 'deleteoffline'});
|
||||
|
@ -95,10 +124,17 @@ export class AddonForumPostOptionsMenuComponent implements OnInit {
|
|||
* Edit a post.
|
||||
*/
|
||||
editPost(): void {
|
||||
if (this.post.id) {
|
||||
if (!this.offlinePost) {
|
||||
this.viewCtrl.dismiss({action: 'edit'});
|
||||
} else {
|
||||
this.viewCtrl.dismiss({action: 'editoffline'});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onlineObserver && this.onlineObserver.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<ion-item text-wrap>
|
||||
<div class="addon-mod-forum-post-title" *ngIf="displaySubject">
|
||||
<h2 text-wrap>
|
||||
<core-icon name="fa-map-pin" *ngIf="post.parent == 0 && post.pinned"></core-icon>
|
||||
<core-icon name="fa-star" class="addon-forum-star" *ngIf="post.parent == 0 && !post.pinned && post.starred"></core-icon>
|
||||
<core-icon name="fa-map-pin" *ngIf="discussion && !post.parentid && discussion.pinned"></core-icon>
|
||||
<core-icon name="fa-star" class="addon-forum-star" *ngIf="discussion && !post.parentid && !discussion.pinned && discussion.starred"></core-icon>
|
||||
<core-format-text [text]="post.subject" contextLevel="module" [contextInstanceId]="forum && forum.cmid" [courseId]="courseId"></core-format-text>
|
||||
</h2>
|
||||
<ion-note float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate">
|
||||
<ion-note float-end padding-left text-end *ngIf="trackPosts && post.unread" [attr.aria-label]="'addon.mod_forum.unread' | translate">
|
||||
<core-icon name="fa-circle" color="primary"></core-icon>
|
||||
</ion-note>
|
||||
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)">
|
||||
|
@ -15,15 +15,15 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="addon-mod-forum-post-info">
|
||||
<ion-avatar *ngIf="post.userfullname" core-user-avatar [user]="post" item-start [courseId]="courseId"></ion-avatar>
|
||||
<ion-avatar *ngIf="post.author && post.author.fullname" core-user-avatar [user]="post.author" item-start [courseId]="courseId"></ion-avatar>
|
||||
<div class="addon-mod-forum-post-author">
|
||||
<h3 *ngIf="post.userfullname">{{post.userfullname}}</h3>
|
||||
<p *ngIf="post.groupname"><ion-icon name="people"></ion-icon> {{ post.groupname }}</p>
|
||||
<p *ngIf="post.modified">{{post.modified * 1000 | coreFormatDate: "strftimerecentfull"}}</p>
|
||||
<p *ngIf="!post.modified"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
|
||||
<h3 *ngIf="post.author && post.author.fullname">{{post.author.fullname}}</h3>
|
||||
<p *ngIf="post.author && post.author.groups"><ng-container *ngFor="let group of post.author.groups"><ion-icon name="people"></ion-icon> {{ group.name }} </ng-container></p>
|
||||
<p *ngIf="post.timecreated">{{post.timecreated * 1000 | coreFormatDate: "strftimerecentfull"}}</p>
|
||||
<p *ngIf="!post.timecreated"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
|
||||
</div>
|
||||
<ng-container *ngIf="!displaySubject">
|
||||
<ion-note float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate">
|
||||
<ion-note float-end padding-left text-end *ngIf="trackPosts && post.unread" [attr.aria-label]="'addon.mod_forum.unread' | translate">
|
||||
<core-icon name="fa-circle" color="primary"></core-icon>
|
||||
</ion-note>
|
||||
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)">
|
||||
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
</ion-item>
|
||||
</ion-card-header>
|
||||
<ion-card-content [attr.padding-top]="post.parent == 0 || null">
|
||||
<ion-card-content [attr.padding-top]="post.parentid == 0 || null">
|
||||
<div padding-bottom *ngIf="post.isprivatereply">
|
||||
<ion-note color="danger">{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note>
|
||||
</div>
|
||||
|
@ -47,17 +47,17 @@
|
|||
<div item-start>{{ 'core.tag.tags' | translate }}:</div>
|
||||
<core-tag-list [tags]="post.tags"></core-tag-list>
|
||||
</ion-item>
|
||||
<core-rating-rate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" [userId]="post.userid" (onUpdate)="ratingUpdated()"></core-rating-rate>
|
||||
<core-rating-rate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" [userId]="post.author.id" (onUpdate)="ratingUpdated()"></core-rating-rate>
|
||||
<core-rating-aggregate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"></core-rating-aggregate>
|
||||
|
||||
<ion-item no-padding text-end *ngIf="post.id && post.canreply && !post.isprivatereply" class="addon-forum-reply-button">
|
||||
<ion-item no-padding text-end *ngIf="post.id > 0 && post.capabilities.reply && !post.isprivatereply" class="addon-forum-reply-button">
|
||||
<button ion-button icon-left clear small (click)="showReplyForm()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id">
|
||||
<core-icon name="fa-reply"></core-icon> {{ 'addon.mod_forum.reply' | translate }}
|
||||
</button>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<form ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id && !replyData.isEditing && replyData.replyingTo == post.id) || (!post.id && replyData.isEditing && replyData.replyingTo == post.parent)" #replyFormEl>
|
||||
<form ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id > 0 && !replyData.isEditing && replyData.replyingTo == post.id) || (post.id <=0 && replyData.isEditing && replyData.replyingTo == post.parentid)" #replyFormEl>
|
||||
<ion-item>
|
||||
<ion-label stacked>{{ 'addon.mod_forum.subject' | translate }}</ion-label>
|
||||
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject" name="subject"></ion-input>
|
||||
|
@ -70,13 +70,15 @@
|
|||
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>
|
||||
<ion-checkbox item-end [(ngModel)]="replyData.isprivatereply" name="isprivatereply"></ion-checkbox>
|
||||
</ion-item>
|
||||
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
|
||||
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>
|
||||
<core-icon *ngIf="advanced" name="fa-caret-down" item-start></core-icon>
|
||||
{{ 'addon.mod_forum.advanced' | translate }}
|
||||
</ion-item-divider>
|
||||
<ng-container *ngIf="advanced">
|
||||
<core-attachments *ngIf="forum.id && forum.maxattachments > 0" [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments>
|
||||
<ng-container *ngIf="forum.id && forum.maxattachments > 0">
|
||||
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
|
||||
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>
|
||||
<core-icon *ngIf="advanced" name="fa-caret-down" item-start></core-icon>
|
||||
{{ 'addon.mod_forum.advanced' | translate }}
|
||||
</ion-item-divider>
|
||||
<ng-container *ngIf="advanced">
|
||||
<core-attachments [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
|
|
|
@ -43,6 +43,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
@Input() post: any; // Post.
|
||||
@Input() courseId: number; // Post's course ID.
|
||||
@Input() discussionId: number; // Post's' discussion ID.
|
||||
@Input() discussion?: any; // Post's' discussion, only for starting posts.
|
||||
@Input() component: string; // Component this post belong to.
|
||||
@Input() componentId: number; // Component ID.
|
||||
@Input() replyData: any; // Object with the new post data. Usually shared between posts.
|
||||
|
@ -92,16 +93,16 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.uniqueId = this.post.id ? 'reply' + this.post.id : 'edit' + this.post.parent;
|
||||
this.uniqueId = this.post.id > 0 ? 'reply' + this.post.id : 'edit' + this.post.parentid;
|
||||
|
||||
const reTranslated = this.translate.instant('addon.mod_forum.re');
|
||||
this.displaySubject = !this.parentSubject ||
|
||||
(this.post.subject != this.parentSubject && this.post.subject != `Re: ${this.parentSubject}` &&
|
||||
this.post.subject != `${reTranslated} ${this.parentSubject}`);
|
||||
this.defaultReplySubject = (this.post.subject.startsWith('Re: ') || this.post.subject.startsWith(reTranslated))
|
||||
? this.post.subject : `${reTranslated} ${this.post.subject}`;
|
||||
this.defaultReplySubject = this.post.replysubject || ((this.post.subject.startsWith('Re: ') ||
|
||||
this.post.subject.startsWith(reTranslated)) ? this.post.subject : `${reTranslated} ${this.post.subject}`);
|
||||
|
||||
this.optionsMenuEnabled = !this.post.id || (this.forumProvider.isGetDiscussionPostAvailable() &&
|
||||
this.optionsMenuEnabled = this.post.id < 0 || (this.forumProvider.isGetDiscussionPostAvailable() &&
|
||||
(this.forumProvider.isDeletePostAvailable() || this.forumProvider.isUpdatePostAvailable()));
|
||||
}
|
||||
|
||||
|
@ -192,7 +193,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
|
||||
const popover = this.popoverCtrl.create(AddonForumPostOptionsMenuComponent, {
|
||||
post: this.post,
|
||||
forumId: this.forum.id
|
||||
forumId: this.forum.id,
|
||||
cmId: this.forum.cmid,
|
||||
});
|
||||
popover.onDidDismiss((data) => {
|
||||
if (data && data.action) {
|
||||
|
@ -328,7 +330,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
this.syncId = this.forumSync.getDiscussionSyncId(this.discussionId);
|
||||
this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
||||
|
||||
this.setReplyFormData(this.post.parent, true, this.post.subject, this.post.message, this.post.attachments,
|
||||
this.setReplyFormData(this.post.parentid, true, this.post.subject, this.post.message, this.post.attachments,
|
||||
this.post.isprivatereply);
|
||||
}).catch(() => {
|
||||
// Cancelled.
|
||||
|
@ -460,9 +462,9 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
this.domUtils.showDeleteConfirm().then(() => {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.forumOffline.deleteReply(this.post.parent));
|
||||
promises.push(this.forumOffline.deleteReply(this.post.parentid));
|
||||
if (this.forum.id) {
|
||||
promises.push(this.forumHelper.deleteReplyStoredFiles(this.forum.id, this.post.parent).catch(() => {
|
||||
promises.push(this.forumHelper.deleteReplyStoredFiles(this.forum.id, this.post.parentid).catch(() => {
|
||||
// Ignore errors, maybe there are no files.
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<ion-header>
|
||||
<ion-navbar core-back-button>
|
||||
<ion-title *ngIf="discussion"><core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId"></core-format-text></ion-title>
|
||||
<ion-title *ngIf="startingPost"><core-format-text [text]="startingPost.subject" contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId"></core-format-text></ion-title>
|
||||
<ion-buttons end>
|
||||
<!-- The context menu will be added in here. -->
|
||||
</ion-buttons>
|
||||
|
@ -41,14 +41,14 @@
|
|||
<core-icon name="fa-lock"></core-icon> {{ 'addon.mod_forum.discussionlocked' | translate }}
|
||||
</ion-card>
|
||||
|
||||
<div *ngIf="discussion" margin-bottom class="highlight">
|
||||
<addon-mod-forum-post [post]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
|
||||
<div *ngIf="startingPost" margin-bottom class="highlight">
|
||||
<addon-mod-forum-post [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
|
||||
</div>
|
||||
|
||||
<ion-card *ngIf="sort != 'nested'">
|
||||
<ng-container *ngFor="let post of posts; first as first">
|
||||
<ion-item-divider *ngIf="!first"></ion-item-divider>
|
||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parent]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
|
||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
|
||||
</ng-container>
|
||||
</ion-card>
|
||||
|
||||
|
@ -60,7 +60,7 @@
|
|||
|
||||
<ng-template #nestedPosts let-post="post">
|
||||
<ion-card>
|
||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parent]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
|
||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
|
||||
</ion-card>
|
||||
<div padding-left *ngIf="post.children.length && post.children[0].subject">
|
||||
<ng-container *ngFor="let child of post.children">
|
||||
|
|
|
@ -52,6 +52,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
forum: any = {};
|
||||
accessInfo: any = {};
|
||||
discussion: any;
|
||||
startingPost: any;
|
||||
posts: any[];
|
||||
discussionLoaded = false;
|
||||
postSubjects: { [id: string]: string };
|
||||
|
@ -85,6 +86,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
|
||||
protected forumId: number;
|
||||
protected postId: number;
|
||||
protected parent: number;
|
||||
protected onlineObserver: any;
|
||||
protected syncObserver: any;
|
||||
protected syncManualObserver: any;
|
||||
|
@ -120,6 +122,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
this.discussionId = this.discussion ? this.discussion.discussion : navParams.get('discussionId');
|
||||
this.trackPosts = navParams.get('trackPosts');
|
||||
this.postId = navParams.get('postId');
|
||||
this.parent = navParams.get('parent');
|
||||
|
||||
this.isOnline = this.appProvider.isOnline();
|
||||
this.onlineObserver = network.onchange().subscribe(() => {
|
||||
|
@ -136,47 +139,67 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad(): void {
|
||||
this.sitesProvider.getCurrentSite().getLocalSiteConfig('AddonModForumDiscussionSort').catch(() => {
|
||||
this.userProvider.getUserPreference('forum_displaymode').catch(() => {
|
||||
// Ignore errors.
|
||||
}).then((value) => {
|
||||
const sortValue = value && parseInt(value, 10);
|
||||
async ionViewDidLoad(): Promise<void> {
|
||||
if (this.parent) {
|
||||
this.sort = 'nested'; // Force nested order.
|
||||
} else {
|
||||
this.sort = await this.getUserSort();
|
||||
}
|
||||
|
||||
switch (sortValue) {
|
||||
await this.fetchPosts(true, false, true);
|
||||
|
||||
const scrollTo = this.postId || this.parent;
|
||||
if (scrollTo) {
|
||||
// Scroll to the post.
|
||||
setTimeout(() => {
|
||||
this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_forum-post-' + scrollTo);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sort type configured by the current user.
|
||||
*
|
||||
* @return Promise resolved with the sort type.
|
||||
*/
|
||||
protected async getUserSort(): Promise<SortType> {
|
||||
try {
|
||||
const value = await this.sitesProvider.getCurrentSite().getLocalSiteConfig('AddonModForumDiscussionSort');
|
||||
|
||||
return value;
|
||||
} catch (error) {
|
||||
try {
|
||||
const value = await this.userProvider.getUserPreference('forum_displaymode');
|
||||
|
||||
switch (Number(value)) {
|
||||
case 1:
|
||||
this.sort = 'flat-oldest';
|
||||
break;
|
||||
return 'flat-oldest';
|
||||
case -1:
|
||||
this.sort = 'flat-newest';
|
||||
break;
|
||||
return 'flat-newest';
|
||||
case 3:
|
||||
this.sort = 'nested';
|
||||
break;
|
||||
return 'nested';
|
||||
case 2: // Threaded not implemented.
|
||||
default:
|
||||
// Not set, use default sort.
|
||||
// @TODO add fallback to $CFG->forum_displaymode.
|
||||
}
|
||||
});
|
||||
}).then((value) => {
|
||||
this.sort = value;
|
||||
}).finally(() => {
|
||||
this.fetchPosts(true, false, true).then(() => {
|
||||
if (this.postId) {
|
||||
// Scroll to the post.
|
||||
setTimeout(() => {
|
||||
this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_forum-post-' + this.postId);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
return 'flat-oldest';
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page that contains the component.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
if (this.syncObserver) {
|
||||
// Already setup.
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh data if this discussion is synchronized automatically.
|
||||
this.syncObserver = this.eventsProvider.on(AddonModForumSyncProvider.AUTO_SYNCED, (data) => {
|
||||
if (data.forumId == this.forumId && this.discussionId == data.discussionId
|
||||
|
@ -231,7 +254,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
}
|
||||
|
||||
if (typeof data.deleted != 'undefined' && data.deleted) {
|
||||
if (data.post.parent == 0) {
|
||||
if (!data.post.parentid) {
|
||||
if (this.svComponent && this.svComponent.isOn()) {
|
||||
this.svComponent.emptyDetails();
|
||||
} else {
|
||||
|
@ -306,9 +329,11 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
let ratingInfo;
|
||||
|
||||
return syncPromise.then(() => {
|
||||
return this.forumProvider.getDiscussionPosts(this.discussionId).then((response) => {
|
||||
return this.forumProvider.getDiscussionPosts(this.discussionId, {cmId: this.cmId}).then((response) => {
|
||||
onlinePosts = response.posts;
|
||||
ratingInfo = response.ratinginfo;
|
||||
this.courseId = response.courseid || this.courseId;
|
||||
this.forumId = response.forumid || this.forumId;
|
||||
}).then(() => {
|
||||
// Check if there are responses stored in offline.
|
||||
return this.forumOffline.getDiscussionReplies(this.discussionId).then((replies) => {
|
||||
|
@ -319,7 +344,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
const posts = {};
|
||||
onlinePosts.forEach((post) => {
|
||||
posts[post.id] = post;
|
||||
hasUnreadPosts = hasUnreadPosts || !post.postread;
|
||||
hasUnreadPosts = hasUnreadPosts || !!post.unread;
|
||||
});
|
||||
|
||||
replies.forEach((offlineReply) => {
|
||||
|
@ -335,7 +360,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
offlineReplies.push(reply);
|
||||
|
||||
// Disable reply of the parent. Reply in offline to the same post is not allowed, edit instead.
|
||||
posts[reply.parent].canreply = false;
|
||||
posts[reply.parentid].capabilities.reply = false;
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -348,18 +373,15 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
}).then(() => {
|
||||
let posts = offlineReplies.concat(onlinePosts);
|
||||
|
||||
const startingPost = this.forumProvider.extractStartingPost(posts);
|
||||
if (startingPost) {
|
||||
// Update discussion data from first post.
|
||||
this.discussion = Object.assign(this.discussion || {}, startingPost);
|
||||
}
|
||||
this.startingPost = this.forumProvider.extractStartingPost(posts);
|
||||
|
||||
// If sort type is nested, normal sorting is disabled and nested posts will be displayed.
|
||||
if (this.sort == 'nested') {
|
||||
// Sort first by creation date to make format tree work.
|
||||
this.forumProvider.sortDiscussionPosts(posts, 'ASC');
|
||||
|
||||
posts = this.utils.formatTree(posts, 'parent', 'id', this.discussion.id);
|
||||
const rootId = this.startingPost ? this.startingPost.id : (this.discussion ? this.discussion.id : 0);
|
||||
posts = this.utils.formatTree(posts, 'parentid', 'id', rootId);
|
||||
} else {
|
||||
// Set default reply subject.
|
||||
const direction = this.sort == 'flat-newest' ? 'DESC' : 'ASC';
|
||||
|
@ -381,50 +403,52 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.forumProvider.getAccessInformation(this.forumId).then((accessInfo) => {
|
||||
promises.push(this.forumProvider.getAccessInformation(this.forumId, {cmId: this.cmId}).then((accessInfo) => {
|
||||
this.accessInfo = accessInfo;
|
||||
|
||||
// Disallow replying if cut-off date is reached and the user has not the capability to override it.
|
||||
// Just in case the posts were fetched from WS when the cut-off date was not reached but it is now.
|
||||
if (this.forumHelper.isCutoffDateReached(forum) && !accessInfo.cancanoverridecutoff) {
|
||||
posts.forEach((post) => {
|
||||
post.canreply = false;
|
||||
post.capabilities.reply = false;
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// The discussion object was not passed as parameter and there is no starting post. Should not happen.
|
||||
if (!this.discussion) {
|
||||
promises.push(this.loadDiscussion(this.forumId, this.discussionId));
|
||||
promises.push(this.loadDiscussion(this.forumId, this.cmId, this.discussionId));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
|
||||
if (!this.discussion) {
|
||||
if (!this.discussion && !this.startingPost) {
|
||||
// The discussion object was not passed as parameter and there is no starting post. Should not happen.
|
||||
return Promise.reject('Invalid forum discussion.');
|
||||
}
|
||||
|
||||
if (this.discussion.userfullname && this.discussion.parent == 0 && this.forum.type == 'single') {
|
||||
// Hide author for first post and type single.
|
||||
this.discussion.userfullname = null;
|
||||
if (this.startingPost.author && this.forum.type == 'single') {
|
||||
// Hide author and groups for first post and type single.
|
||||
this.startingPost.author.fullname = null;
|
||||
this.startingPost.author.groups = null;
|
||||
|
||||
}
|
||||
|
||||
this.posts = posts;
|
||||
this.ratingInfo = ratingInfo;
|
||||
|
||||
this.postSubjects = this.getAllPosts().reduce((postSubjects, post) => {
|
||||
postSubjects[post.id] = post.subject;
|
||||
|
||||
return postSubjects;
|
||||
}, { [this.discussion.id]: this.discussion.subject });
|
||||
}, { [this.startingPost.id]: this.startingPost.subject });
|
||||
});
|
||||
}).then(() => {
|
||||
if (this.forumProvider.isSetPinStateAvailableForSite()) {
|
||||
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
||||
return this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => {
|
||||
return this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => {
|
||||
this.canPin = !!response.canpindiscussions;
|
||||
}).catch(() => {
|
||||
this.canPin = false;
|
||||
|
@ -462,13 +486,14 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
* Convenience function to load discussion.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param cmId Forum cmid.
|
||||
* @param discussionId Discussion ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected loadDiscussion(forumId: number, discussionId: number): Promise<void> {
|
||||
protected loadDiscussion(forumId: number, cmId: number, discussionId: number): Promise<void> {
|
||||
// Fetch the discussion if not passed as parameter.
|
||||
if (!this.discussion && forumId) {
|
||||
return this.forumHelper.getDiscussionById(forumId, discussionId).then((discussion) => {
|
||||
return this.forumHelper.getDiscussionById(forumId, cmId, discussionId).then((discussion) => {
|
||||
this.discussion = discussion;
|
||||
this.discussionId = this.discussion.discussion;
|
||||
}).catch(() => {
|
||||
|
@ -688,6 +713,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
||||
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
||||
this.changeDiscObserver && this.changeDiscObserver.off();
|
||||
delete this.syncObserver;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -722,5 +748,4 @@ export class AddonModForumDiscussionPage implements OnDestroy {
|
|||
|
||||
return posts;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -112,6 +112,11 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
|||
* User entered the page that contains the component.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
if (this.syncObserver) {
|
||||
// Already setup.
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh data if this discussion is synchronized automatically.
|
||||
this.syncObserver = this.eventsProvider.on(AddonModForumSyncProvider.AUTO_SYNCED, (data) => {
|
||||
if (data.forumId == this.forumId && data.userId == this.sitesProvider.getCurrentSiteUserId()) {
|
||||
|
@ -171,7 +176,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
|||
this.newDiscussion.postToAllGroups = false;
|
||||
|
||||
// Use the canAddDiscussion WS to check if the user can add attachments and pin discussions.
|
||||
promises.push(this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => {
|
||||
promises.push(this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => {
|
||||
this.canPin = !!response.canpindiscussions;
|
||||
this.canCreateAttachments = !!response.cancreateattachment;
|
||||
}).catch(() => {
|
||||
|
@ -185,7 +190,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
|||
}));
|
||||
|
||||
// Get access information.
|
||||
promises.push(this.forumProvider.getAccessInformation(this.forumId).then((accessInfo) => {
|
||||
promises.push(this.forumProvider.getAccessInformation(this.forumId, {cmId: this.cmId}).then((accessInfo) => {
|
||||
this.accessInfo = accessInfo;
|
||||
}));
|
||||
|
||||
|
@ -260,7 +265,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
|||
*/
|
||||
protected validateVisibleGroups(forumGroups: any[]): Promise<any[]> {
|
||||
// We first check if the user can post to all the groups.
|
||||
return this.forumProvider.canAddDiscussionToAll(this.forumId).catch(() => {
|
||||
return this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).catch(() => {
|
||||
// The call failed, let's assume he can't.
|
||||
return {
|
||||
status: false,
|
||||
|
@ -280,7 +285,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
|||
const filtered = [];
|
||||
|
||||
forumGroups.forEach((group) => {
|
||||
promises.push(this.forumProvider.canAddDiscussion(this.forumId, group.id).catch(() => {
|
||||
promises.push(this.forumProvider.canAddDiscussion(this.forumId, group.id, {cmId: this.cmId}).catch(() => {
|
||||
/* The call failed, let's return true so the group is shown. If the user can't post to
|
||||
it an error will be shown when he tries to add the discussion. */
|
||||
return {
|
||||
|
@ -337,7 +342,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
|||
|
||||
if (check) {
|
||||
// We need to check if the user can add a discussion to all participants.
|
||||
promise = this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => {
|
||||
promise = this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => {
|
||||
this.canPin = !!response.canpindiscussions;
|
||||
this.canCreateAttachments = !!response.cancreateattachment;
|
||||
|
||||
|
@ -549,6 +554,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
|||
*/
|
||||
ionViewWillLeave(): void {
|
||||
this.syncObserver && this.syncObserver.off();
|
||||
delete this.syncObserver;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -60,6 +60,9 @@ export class AddonModForumDiscussionLinkHandler extends CoreContentLinksHandlerB
|
|||
if (data.postid || params.urlHash) {
|
||||
pageParams.postId = parseInt(data.postid || params.urlHash.replace('p', ''));
|
||||
}
|
||||
if (params.parent) {
|
||||
pageParams.parent = parseInt(params.parent);
|
||||
}
|
||||
|
||||
this.linkHelper.goInSite(navCtrl, 'AddonModForumDiscussionPage', pageParams, siteId);
|
||||
}
|
||||
|
|
|
@ -14,16 +14,18 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites';
|
||||
import { CoreUserProvider } from '@core/user/providers/user';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||
import { AddonModForumOfflineProvider } from './offline';
|
||||
import { CoreRatingInfo } from '@core/rating/providers/rating';
|
||||
import { CoreCourseCommonModWSOptions } from '@core/course/providers/course';
|
||||
import { CoreUrlUtils } from '@providers/utils/url';
|
||||
|
||||
/**
|
||||
* Service that provides some features for forums.
|
||||
|
@ -206,26 +208,29 @@ export class AddonModForumProvider {
|
|||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param groupId Group ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with an object with the following properties:
|
||||
* - status (boolean)
|
||||
* - canpindiscussions (boolean)
|
||||
* - cancreateattachment (boolean)
|
||||
*/
|
||||
canAddDiscussion(forumId: number, groupId: number, siteId?: string): Promise<any> {
|
||||
canAddDiscussion(forumId: number, groupId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
const params = {
|
||||
forumid: forumId,
|
||||
groupid: groupId
|
||||
groupid: groupId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getCanAddDiscussionCacheKey(forumId, groupId)
|
||||
cacheKey: this.getCanAddDiscussionCacheKey(forumId, groupId),
|
||||
component: AddonModForumProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
return site.read('mod_forum_can_add_discussion', params, preSets).then((result) => {
|
||||
if (result) {
|
||||
if (typeof result.canpindiscussions == 'undefined') {
|
||||
// WS doesn't support it yet, default it to false to prevent students from seing the option.
|
||||
// WS doesn't support it yet, default it to false to prevent students from seeing the option.
|
||||
result.canpindiscussions = false;
|
||||
}
|
||||
if (typeof result.cancreateattachment == 'undefined') {
|
||||
|
@ -245,14 +250,14 @@ export class AddonModForumProvider {
|
|||
* Check if a user can post to all groups.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with an object with the following properties:
|
||||
* - status (boolean)
|
||||
* - canpindiscussions (boolean)
|
||||
* - cancreateattachment (boolean)
|
||||
*/
|
||||
canAddDiscussionToAll(forumId: number, siteId?: string): Promise<any> {
|
||||
return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, siteId);
|
||||
canAddDiscussionToAll(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -280,7 +285,7 @@ export class AddonModForumProvider {
|
|||
* @return Starting post or undefined if not found.
|
||||
*/
|
||||
extractStartingPost(posts: any[]): any {
|
||||
const index = posts.findIndex((post) => post.parent == 0);
|
||||
const index = posts.findIndex((post) => !post.parentid);
|
||||
|
||||
return index >= 0 ? posts.splice(index, 1).pop() : undefined;
|
||||
}
|
||||
|
@ -305,6 +310,18 @@ export class AddonModForumProvider {
|
|||
return this.sitesProvider.wsAvailableInCurrentSite('mod_forum_get_discussion_post');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not getDiscussionPost WS available or not.
|
||||
*
|
||||
* @param site Site. If not defined, current site.
|
||||
* @return If WS is avalaible.
|
||||
* @since 3.7
|
||||
*/
|
||||
isGetDiscussionPostsAvailable(site?: CoreSite): boolean {
|
||||
return site ? site.wsAvailable('mod_forum_get_discussion_posts') :
|
||||
this.sitesProvider.wsAvailableInCurrentSite('mod_forum_get_discussion_posts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not deletePost WS available or not.
|
||||
*
|
||||
|
@ -370,17 +387,19 @@ export class AddonModForumProvider {
|
|||
* Get all course forums.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the forums are retrieved.
|
||||
*/
|
||||
getCourseForums(courseId: number, siteId?: string): Promise<any[]> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise<any[]> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
courseids: [courseId],
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getForumDataCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModForumProvider.COMPONENT,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_forum_get_forums_by_courses', params, preSets);
|
||||
|
@ -393,24 +412,23 @@ export class AddonModForumProvider {
|
|||
* @param forumId Forum ID.
|
||||
* @param discussionId Discussion ID.
|
||||
* @param postId Post ID.
|
||||
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the post is retrieved.
|
||||
*/
|
||||
getDiscussionPost(forumId: number, discussionId: number, postId: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
postid: postId
|
||||
},
|
||||
preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId),
|
||||
updateFrequency: CoreSite.FREQUENCY_USUALLY
|
||||
};
|
||||
getDiscussionPost(forumId: number, discussionId: number, postId: number, options: CoreCourseCommonModWSOptions = {})
|
||||
: Promise<any> {
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets.getFromCache = false;
|
||||
preSets.emergencyCache = false;
|
||||
}
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const params = {
|
||||
postid: postId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId),
|
||||
updateFrequency: CoreSite.FREQUENCY_USUALLY,
|
||||
component: AddonModForumProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_forum_get_discussion_post', params, preSets).then((response) => {
|
||||
if (response.post) {
|
||||
|
@ -427,11 +445,11 @@ export class AddonModForumProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param cmId Course module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the forum is retrieved.
|
||||
*/
|
||||
getForum(courseId: number, cmId: number, siteId?: string): Promise<any> {
|
||||
return this.getCourseForums(courseId, siteId).then((forums) => {
|
||||
getForum(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
||||
return this.getCourseForums(courseId, options).then((forums) => {
|
||||
const forum = forums.find((forum) => forum.cmid == cmId);
|
||||
if (forum) {
|
||||
return forum;
|
||||
|
@ -446,11 +464,11 @@ export class AddonModForumProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @param forumId Forum ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the forum is retrieved.
|
||||
*/
|
||||
getForumById(courseId: number, forumId: number, siteId?: string): Promise<any> {
|
||||
return this.getCourseForums(courseId, siteId).then((forums) => {
|
||||
getForumById(courseId: number, forumId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
||||
return this.getCourseForums(courseId, options).then((forums) => {
|
||||
const forum = forums.find((forum) => forum.id == forumId);
|
||||
if (forum) {
|
||||
return forum;
|
||||
|
@ -464,24 +482,25 @@ export class AddonModForumProvider {
|
|||
* Get access information for a given forum.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param forceCache True to always get the value from cache. false otherwise.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Object with access information.
|
||||
* @since 3.7
|
||||
*/
|
||||
getAccessInformation(forumId: number, forceCache?: boolean, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
getAccessInformation(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
if (!site.wsAvailable('mod_forum_get_forum_access_information')) {
|
||||
// Access information not available for 3.6 or older sites.
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
const params = {
|
||||
forumid: forumId
|
||||
forumid: forumId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getAccessInformationCacheKey(forumId),
|
||||
omitExpires: forceCache
|
||||
component: AddonModForumProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('mod_forum_get_forum_access_information', params, preSets);
|
||||
|
@ -492,20 +511,91 @@ export class AddonModForumProvider {
|
|||
* Get forum discussion posts.
|
||||
*
|
||||
* @param discussionId Discussion ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with forum posts and rating info.
|
||||
*/
|
||||
getDiscussionPosts(discussionId: number, siteId?: string): Promise<{posts: any[], ratinginfo?: CoreRatingInfo}> {
|
||||
const params = {
|
||||
discussionid: discussionId
|
||||
getDiscussionPosts(discussionId: number, options: CoreCourseCommonModWSOptions = {}): Promise<{posts: any[], courseid?: number,
|
||||
forumid?: number, ratinginfo?: CoreRatingInfo}> {
|
||||
|
||||
// Convenience function to translate legacy data to new format.
|
||||
const translateLegacyPostsFormat = (posts: any[]): any[] => {
|
||||
return posts.map((post) => {
|
||||
const newPost = {
|
||||
id: post.id ,
|
||||
discussionid: post.discussion,
|
||||
parentid: post.parent,
|
||||
hasparent: !!post.parent,
|
||||
author: {
|
||||
id: post.userid,
|
||||
fullname: post.userfullname,
|
||||
urls: { profileimage: post.userpictureurl },
|
||||
},
|
||||
timecreated: post.created,
|
||||
subject: post.subject,
|
||||
message: post.message,
|
||||
attachments : post.attachments,
|
||||
capabilities: {
|
||||
reply: !!post.canreply,
|
||||
},
|
||||
|
||||
unread: !post.postread,
|
||||
isprivatereply: !!post.isprivatereply,
|
||||
tags: post.tags,
|
||||
};
|
||||
|
||||
if (post.groupname) {
|
||||
newPost.author['groups'] = [{name: post.groupname}];
|
||||
}
|
||||
|
||||
return newPost;
|
||||
});
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getDiscussionPostsCacheKey(discussionId)
|
||||
// For some reason, the new WS doesn't use the tags exporter so it returns a different format than other WebServices.
|
||||
// Convert the new format to the exporter one so it's the same as in other WebServices.
|
||||
const translateTagsFormatToLegacy = (posts: any[]): any[] => {
|
||||
posts.forEach((post) => {
|
||||
post.tags = post.tags.map((tag) => {
|
||||
const viewUrl = (tag.urls && tag.urls.view) || '';
|
||||
const params = CoreUrlUtils.instance.extractUrlParams(viewUrl);
|
||||
|
||||
return {
|
||||
id: tag.tagid,
|
||||
taginstanceid: tag.id,
|
||||
flag: tag.flag ? 1 : 0,
|
||||
isstandard: tag.isstandard,
|
||||
rawname: tag.displayname,
|
||||
name: tag.displayname,
|
||||
tagcollid: params.tc ? Number(params.tc) : undefined,
|
||||
taginstancecontextid: params.from ? Number(params.from) : undefined,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return posts;
|
||||
};
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.read('mod_forum_get_forum_discussion_posts', params, preSets).then((response) => {
|
||||
const params = {
|
||||
discussionid: discussionId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getDiscussionPostsCacheKey(discussionId),
|
||||
component: AddonModForumProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
const wsName = this.isGetDiscussionPostsAvailable(site) ? 'mod_forum_get_discussion_posts' :
|
||||
'mod_forum_get_forum_discussion_posts';
|
||||
|
||||
return site.read(wsName, params, preSets).then((response) => {
|
||||
if (response) {
|
||||
|
||||
if (wsName == 'mod_forum_get_forum_discussion_posts') {
|
||||
response.posts = translateLegacyPostsFormat(response.posts);
|
||||
} else {
|
||||
response.posts = translateTagsFormatToLegacy(response.posts);
|
||||
}
|
||||
this.storeUserData(response.posts);
|
||||
|
||||
return response;
|
||||
|
@ -525,8 +615,13 @@ export class AddonModForumProvider {
|
|||
sortDiscussionPosts(posts: any[], direction: string): void {
|
||||
// @todo: Check children when sorting.
|
||||
posts.sort((a, b) => {
|
||||
a = parseInt(a.created, 10);
|
||||
b = parseInt(b.created, 10);
|
||||
a = parseInt(a.timecreated, 10) || 0;
|
||||
b = parseInt(b.timecreated, 10) || 0;
|
||||
if (a == 0 || b == 0) {
|
||||
// Leave 0 at the end.
|
||||
return b - a;
|
||||
}
|
||||
|
||||
if (direction == 'ASC') {
|
||||
return a - b;
|
||||
} else {
|
||||
|
@ -592,32 +687,30 @@ export class AddonModForumProvider {
|
|||
* Get forum discussions.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param sortOrder Sort order.
|
||||
* @param page Page.
|
||||
* @param forceCache True to always get the value from cache. false otherwise.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with an object with:
|
||||
* - discussions: List of discussions. Note that for every discussion in the list discussion.id is the main post ID but
|
||||
* discussion ID is discussion.discussion.
|
||||
* - canLoadMore: True if there may be more discussions to load.
|
||||
*/
|
||||
getDiscussions(forumId: number, sortOrder?: number, page: number = 0, forceCache?: boolean, siteId?: string): Promise<any> {
|
||||
sortOrder = sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC;
|
||||
getDiscussions(forumId: number, options: AddonModForumGetDiscussionsOptions = {}): Promise<any> {
|
||||
options.sortOrder = options.sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC;
|
||||
options.page = options.page || 0;
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
||||
let method = 'mod_forum_get_forum_discussions_paginated';
|
||||
const params: any = {
|
||||
forumid: forumId,
|
||||
page: page,
|
||||
perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE
|
||||
page: options.page,
|
||||
perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE,
|
||||
};
|
||||
|
||||
if (site.wsAvailable('mod_forum_get_forum_discussions')) {
|
||||
// Since Moodle 3.7.
|
||||
method = 'mod_forum_get_forum_discussions';
|
||||
params.sortorder = sortOrder;
|
||||
params.sortorder = options.sortOrder;
|
||||
} else {
|
||||
if (sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) {
|
||||
if (options.sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) {
|
||||
params.sortby = 'timemodified';
|
||||
params.sortdirection = 'DESC';
|
||||
} else {
|
||||
|
@ -625,29 +718,27 @@ export class AddonModForumProvider {
|
|||
return Promise.reject(null);
|
||||
}
|
||||
}
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getDiscussionsListCacheKey(forumId, sortOrder)
|
||||
|
||||
const preSets = {
|
||||
cacheKey: this.getDiscussionsListCacheKey(forumId, options.sortOrder),
|
||||
component: AddonModForumProvider.COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
if (forceCache) {
|
||||
preSets.omitExpires = true;
|
||||
}
|
||||
|
||||
return site.read(method, params, preSets).catch((error) => {
|
||||
// Try to get the data from cache stored with the old WS method.
|
||||
if (!this.appProvider.isOnline() && method == 'mod_forum_get_forum_discussion' &&
|
||||
sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) {
|
||||
options.sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) {
|
||||
|
||||
const params = {
|
||||
forumid: forumId,
|
||||
page: page,
|
||||
page: options.page,
|
||||
perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE,
|
||||
sortby: 'timemodified',
|
||||
sortdirection: 'DESC'
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getDiscussionsListCacheKey(forumId, sortOrder),
|
||||
omitExpires: true
|
||||
};
|
||||
Object.assign(preSets, this.sitesProvider.getReadingStrategyPreSets(CoreSitesReadingStrategy.PreferCache));
|
||||
|
||||
return site.read('mod_forum_get_forum_discussions_paginated', params, preSets);
|
||||
}
|
||||
|
@ -673,6 +764,7 @@ export class AddonModForumProvider {
|
|||
* If a page fails, the discussions until that page will be returned along with a flag indicating an error occurred.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param cmId Forum cmid.
|
||||
* @param sortOrder Sort order.
|
||||
* @param forceCache True to always get the value from cache, false otherwise.
|
||||
* @param numPages Number of pages to get. If not defined, all pages.
|
||||
|
@ -682,17 +774,14 @@ export class AddonModForumProvider {
|
|||
* - discussions: List of discussions.
|
||||
* - error: True if an error occurred, false otherwise.
|
||||
*/
|
||||
getDiscussionsInPages(forumId: number, sortOrder?: number, forceCache?: boolean, numPages?: number, startPage?: number,
|
||||
siteId?: string): Promise<any> {
|
||||
if (typeof numPages == 'undefined') {
|
||||
numPages = -1;
|
||||
}
|
||||
startPage = startPage || 0;
|
||||
getDiscussionsInPages(forumId: number, options: AddonModForumGetDiscussionsInPagesOptions = {}): Promise<any> {
|
||||
options.page = options.page || 0;
|
||||
|
||||
const result = {
|
||||
discussions: [],
|
||||
error: false
|
||||
};
|
||||
let numPages = typeof options.numPages == 'undefined' ? -1 : options.numPages;
|
||||
|
||||
if (!numPages) {
|
||||
return Promise.resolve(result);
|
||||
|
@ -700,7 +789,7 @@ export class AddonModForumProvider {
|
|||
|
||||
const getPage = (page: number): Promise<any> => {
|
||||
// Get page discussions.
|
||||
return this.getDiscussions(forumId, sortOrder, page, forceCache, siteId).then((response) => {
|
||||
return this.getDiscussions(forumId, options).then((response) => {
|
||||
result.discussions = result.discussions.concat(response.discussions);
|
||||
numPages--;
|
||||
|
||||
|
@ -717,7 +806,7 @@ export class AddonModForumProvider {
|
|||
});
|
||||
};
|
||||
|
||||
return getPage(startPage);
|
||||
return getPage(options.page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -753,7 +842,11 @@ export class AddonModForumProvider {
|
|||
|
||||
this.getAvailableSortOrders().forEach((sortOrder) => {
|
||||
// We need to get the list of discussions to be able to invalidate their posts.
|
||||
promises.push(this.getDiscussionsInPages(forum.id, sortOrder.value, true).then((response) => {
|
||||
promises.push(this.getDiscussionsInPages(forum.id, {
|
||||
cmId: forum.cmid,
|
||||
sortOrder: sortOrder.value,
|
||||
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||
}).then((response) => {
|
||||
// Now invalidate the WS calls.
|
||||
const promises = [];
|
||||
|
||||
|
@ -1045,6 +1138,16 @@ export class AddonModForumProvider {
|
|||
const users = {};
|
||||
|
||||
list.forEach((entry) => {
|
||||
if (entry.author) {
|
||||
const authorId = parseInt(entry.author.id);
|
||||
if (!isNaN(authorId) && !users[authorId]) {
|
||||
users[authorId] = {
|
||||
id: entry.author.id,
|
||||
fullname: entry.author.fullname,
|
||||
profileimageurl: entry.author.urls.profileimage
|
||||
};
|
||||
}
|
||||
}
|
||||
const userId = parseInt(entry.userid);
|
||||
if (!isNaN(userId) && !users[userId]) {
|
||||
users[userId] = {
|
||||
|
@ -1091,3 +1194,18 @@ export class AddonModForumProvider {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to pass to get discussions.
|
||||
*/
|
||||
export type AddonModForumGetDiscussionsOptions = CoreCourseCommonModWSOptions & {
|
||||
sortOrder?: number; // Sort order.
|
||||
page?: number; // Page. Defaults to 0.
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to pass to get discussions in pages.
|
||||
*/
|
||||
export type AddonModForumGetDiscussionsInPagesOptions = AddonModForumGetDiscussionsOptions & {
|
||||
numPages?: number; // Number of pages to get. If not defined, all pages.
|
||||
};
|
||||
|
|
|
@ -161,24 +161,23 @@ export class AddonModForumHelperProvider {
|
|||
*/
|
||||
convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise<any> {
|
||||
const reply: any = {
|
||||
attachments: [],
|
||||
canreply: false,
|
||||
children: [],
|
||||
created: offlineReply.timecreated,
|
||||
discussion: offlineReply.discussionid,
|
||||
id: false,
|
||||
mailed: 0,
|
||||
mailnow: 0,
|
||||
message: offlineReply.message,
|
||||
messageformat: 1,
|
||||
messagetrust: 0,
|
||||
modified: false,
|
||||
parent: offlineReply.postid,
|
||||
postread: false,
|
||||
id: -offlineReply.timecreated,
|
||||
discussionid: offlineReply.discussionid,
|
||||
parentid: offlineReply.postid,
|
||||
hasparent: !!offlineReply.postid,
|
||||
author: {
|
||||
id: offlineReply.userid,
|
||||
},
|
||||
timecreated: false,
|
||||
subject: offlineReply.subject,
|
||||
totalscore: 0,
|
||||
userid: offlineReply.userid,
|
||||
isprivatereply: offlineReply.options && offlineReply.options.private
|
||||
message: offlineReply.message,
|
||||
attachments: [],
|
||||
capabilities: {
|
||||
reply: false,
|
||||
},
|
||||
unread: false,
|
||||
isprivatereply: offlineReply.options && offlineReply.options.private,
|
||||
tags: null
|
||||
},
|
||||
promises = [];
|
||||
|
||||
|
@ -187,7 +186,7 @@ export class AddonModForumHelperProvider {
|
|||
reply.attachments = offlineReply.options.attachmentsid.online || [];
|
||||
|
||||
if (offlineReply.options.attachmentsid.offline) {
|
||||
promises.push(this.getReplyStoredFiles(offlineReply.forumid, reply.parent, siteId, reply.userid)
|
||||
promises.push(this.getReplyStoredFiles(offlineReply.forumid, reply.parentid, siteId, offlineReply.userid)
|
||||
.then((files) => {
|
||||
reply.attachments = reply.attachments.concat(files);
|
||||
}));
|
||||
|
@ -196,8 +195,8 @@ export class AddonModForumHelperProvider {
|
|||
|
||||
// Get user data.
|
||||
promises.push(this.userProvider.getProfile(offlineReply.userid, offlineReply.courseid, true).then((user) => {
|
||||
reply.userfullname = user.fullname;
|
||||
reply.userpictureurl = user.profileimageurl;
|
||||
reply.author.fullname = user.fullname;
|
||||
reply.author.urls = { profileimage: user.profileimageurl };
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
|
@ -270,15 +269,20 @@ export class AddonModForumHelperProvider {
|
|||
* This function is inefficient because it needs to fetch all discussion pages in the worst case.
|
||||
*
|
||||
* @param forumId Forum ID.
|
||||
* @param cmId Forum cmid
|
||||
* @param discussionId Discussion ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the discussion data.
|
||||
*/
|
||||
getDiscussionById(forumId: number, discussionId: number, siteId?: string): Promise<any> {
|
||||
getDiscussionById(forumId: number, cmId: number, discussionId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const findDiscussion = (page: number): Promise<any> => {
|
||||
return this.forumProvider.getDiscussions(forumId, undefined, page, false, siteId).then((response) => {
|
||||
return this.forumProvider.getDiscussions(forumId, {
|
||||
cmId,
|
||||
page,
|
||||
siteId,
|
||||
}).then((response) => {
|
||||
if (response.discussions && response.discussions.length > 0) {
|
||||
// Note that discussion.id is the main post ID but discussion ID is discussion.discussion.
|
||||
const discussion = response.discussions.find((discussion) => discussion.discussion == discussionId);
|
||||
|
|
|
@ -143,7 +143,7 @@ export class AddonModForumModuleHandler implements CoreCourseModuleHandler {
|
|||
|
||||
this.forumProvider.invalidateForumData(courseId).finally(() => {
|
||||
// Handle unread posts.
|
||||
this.forumProvider.getForum(courseId, moduleId, siteId).then((forumData) => {
|
||||
this.forumProvider.getForum(courseId, moduleId, {siteId}).then((forumData) => {
|
||||
data.extraBadgeColor = '';
|
||||
data.extraBadge = forumData.unreadpostscount ? this.translate.instant('addon.mod_forum.unreadpostsnumber',
|
||||
{$a : forumData.unreadpostscount }) : '';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue