diff --git a/upgrade.txt b/upgrade.txt
index fd745c99c67..69aa3c43088 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -9,6 +9,7 @@ information provided here is intended especially for developers.
* Now mmUser#formatRoleList is returning a String instead of a Promise.
* The event mmCoreEventSessionExpired now receives an object instead of a string. Also, mmLoginLaunchSiteURL and mmLoginLaunchPassport have been deprecated, please use mmLoginLaunchData instead.
* Handlers registered in $mmFileUploaderDelegate now must implement a getData function instead of getController. This is because now file picker uses action sheet instead of a new view.
+ * The function $mmaModForumOffline#convertOfflineReplyToOnline is now in $mmaModForumHelper.
* New Phonegap plugin installed. Please run:
ionic plugin add cordova-universal-clipboard
or
diff --git a/www/addons/mod/assign/services/helper.js b/www/addons/mod/assign/services/helper.js
index ef6a336060b..675d603fab1 100644
--- a/www/addons/mod/assign/services/helper.js
+++ b/www/addons/mod/assign/services/helper.js
@@ -21,7 +21,7 @@ angular.module('mm.addons.mod_assign')
* @ngdoc service
* @name $mmaModAssignHelper
*/
-.factory('$mmaModAssignHelper', function($mmUtil, $mmaModAssignSubmissionDelegate, $q, $mmSite, $mmFS, $mmFilepool, $mmaModAssign,
+.factory('$mmaModAssignHelper', function($mmUtil, $mmaModAssignSubmissionDelegate, $q, $mmSite, $mmFS, $mmaModAssign,
$mmFileUploader, mmaModAssignComponent, $mmaModAssignOffline, $mmaModAssignFeedbackDelegate) {
var self = {};
@@ -374,47 +374,9 @@ angular.module('mm.addons.mod_assign')
self.storeSubmissionFiles = function(assignId, pluginName, files, userId, siteId) {
siteId = siteId || $mmSite.getId();
- var result = {
- online: [],
- offline: 0
- };
-
- if (!files || !files.length) {
- return $q.when(result);
- }
-
+ // Get the folder where to store the files.
return $mmaModAssignOffline.getSubmissionPluginFolder(assignId, pluginName, userId, siteId).then(function(folderPath) {
- // Remove unused files from previous submissions.
- return $mmFS.removeUnusedFiles(folderPath, files).then(function() {
- var promises = [];
-
- angular.forEach(files, function(file) {
- if (file.filename && !file.name) {
- // It's an online file, add it to the result and ignore it.
- result.online.push({
- filename: file.filename,
- fileurl: file.fileurl
- });
- return;
- } else if (!file.name) {
- // Error.
- promises.push($q.reject());
- } else if (file.fullPath && file.fullPath.indexOf(folderPath) != -1) {
- // File already in the submission folder.
- result.offline++;
- } else {
- // Local file, copy it. Use copy instead of move to prevent having a unstable state if
- // some copies succeed and others don't.
- var destFile = $mmFS.concatenatePaths(folderPath, file.name);
- promises.push($mmFS.copyFile(file.toURL(), destFile));
- result.offline++;
- }
- });
-
- return $q.all(promises).then(function() {
- return result;
- });
- });
+ return $mmFileUploader.storeFilesToUpload(folderPath, files);
});
};
@@ -431,30 +393,7 @@ angular.module('mm.addons.mod_assign')
* @return {Promise} Promise resolved with the itemId.
*/
self.uploadFile = function(assignId, file, itemId, siteId) {
- siteId = siteId || $mmSite.getId();
-
- var promise,
- fileName;
-
- if (file.filename && !file.name) {
- // It's an online file. We need to download it and re-upload it.
- fileName = file.filename;
- promise = $mmFilepool.downloadUrl(siteId, file.fileurl, false, mmaModAssignComponent, assignId).then(function(path) {
- return $mmFS.getExternalFile(path);
- });
- } else {
- // Local file, we already have the file entry.
- fileName = file.name;
- promise = $q.when(file);
- }
-
- return promise.then(function(fileEntry) {
- // Now upload the file.
- return $mmFileUploader.uploadGenericFile(fileEntry.toURL(), fileName, fileEntry.type, true, 'draft', itemId, siteId)
- .then(function(result) {
- return result.itemid;
- });
- });
+ return $mmFileUploader.uploadOrReuploadFile(file, itemId, mmaModAssignComponent, assignId, siteId);
};
/**
@@ -471,36 +410,7 @@ angular.module('mm.addons.mod_assign')
* @return {Promise} Promise resolved with the itemId.
*/
self.uploadFiles = function(assignId, files, siteId) {
- siteId = siteId || $mmSite.getId();
-
- if (!files || !files.length) {
- // Return fake draft ID.
- return $q.when(1);
- }
-
- // Upload only the first file first to get a draft id.
- return self.uploadFile(assignId, files[0]).then(function(itemId) {
- var promises = [],
- error;
-
- angular.forEach(files, function(file, index) {
- if (index === 0) {
- // First file has already been uploaded.
- return;
- }
-
- promises.push(self.uploadFile(assignId, file, itemId, siteId).catch(function(message) {
- error = message;
- return $q.reject();
- }));
- });
-
- return $q.all(promises).then(function() {
- return itemId;
- }).catch(function() {
- return $q.reject(error);
- });
- });
+ return $mmFileUploader.uploadOrReuploadFiles(files, mmaModAssignComponent, assignId, siteId);
};
/**
diff --git a/www/addons/mod/forum/controllers/discussion.js b/www/addons/mod/forum/controllers/discussion.js
index b1f5d8578c2..37908676cf8 100644
--- a/www/addons/mod/forum/controllers/discussion.js
+++ b/www/addons/mod/forum/controllers/discussion.js
@@ -23,29 +23,34 @@ angular.module('mm.addons.mod_forum')
*/
.controller('mmaModForumDiscussionCtrl', function($q, $scope, $stateParams, $mmaModForum, $mmSite, $mmUtil, $translate, $mmEvents,
$ionicScrollDelegate, mmaModForumComponent, mmaModForumReplyDiscussionEvent, $mmaModForumOffline, $mmaModForumSync,
- mmaModForumAutomSyncedEvent, mmaModForumManualSyncedEvent, $mmApp, $ionicPlatform, mmCoreEventOnlineStatusChanged) {
+ mmaModForumAutomSyncedEvent, mmaModForumManualSyncedEvent, $mmApp, $ionicPlatform, mmCoreEventOnlineStatusChanged,
+ $mmaModForumHelper) {
var discussionId = $stateParams.discussionid,
- courseid = $stateParams.cid,
+ courseId = $stateParams.cid,
forumId = $stateParams.forumid,
cmid = $stateParams.cmid,
scrollView,
syncObserver, syncManualObserver, onlineObserver;
+ // Block leaving the view, we want to show a confirm to the user if there's unsaved data.
+ $mmUtil.blockLeaveView($scope, leaveView);
+
$scope.discussionId = discussionId;
$scope.trackPosts = $stateParams.trackposts;
$scope.component = mmaModForumComponent;
$scope.discussionStr = $translate.instant('discussion');
$scope.componentId = cmid;
- $scope.courseid = courseid;
+ $scope.courseId = courseId;
$scope.refreshPostsIcon = 'spinner';
$scope.syncIcon = 'spinner';
- $scope.newpost = {
+ $scope.newPost = {
replyingto: undefined,
editing: undefined,
subject: '',
text: '',
- isEditing: false
+ isEditing: false,
+ files: []
};
$scope.sort = {
icon: 'ion-arrow-up-c',
@@ -60,12 +65,14 @@ angular.module('mm.addons.mod_forum')
// Receive locked as param since it's returned by getDiscussions. This means that PullToRefresh won't update this value.
$scope.locked = !!$stateParams.locked;
+ $scope.originalData = {};
+
// Convenience function to get the forum.
function fetchForum() {
- if (courseid && cmid) {
- return $mmaModForum.getForum(courseid, cmid);
- } else if (courseid && forumId) {
- return $mmaModForum.getForumById(courseid, forumId);
+ if (courseId && cmid) {
+ return $mmaModForum.getForum(courseId, cmid);
+ } else if (courseId && forumId) {
+ return $mmaModForum.getForumById(courseId, forumId);
} else {
// Cannot get the forum.
return $q.reject();
@@ -95,44 +102,39 @@ angular.module('mm.addons.mod_forum')
}).then(function() {
// Check if there are responses stored in offline.
- return $mmaModForumOffline.hasDiscussionReplies(discussionId).then(function(hasOffline) {
- $scope.postHasOffline = hasOffline;
-
- if (hasOffline) {
- return $mmaModForumOffline.getDiscussionReplies(discussionId).then(function(replies) {
- var convertPromises = [];
-
- // Index posts to allow quick access.
- var posts = {};
- angular.forEach(onlinePosts, function(post) {
- posts[post.id] = post;
- });
-
- angular.forEach(replies, function(offlineReply) {
- // If we don't have forumId and courseId, get it from the post.
- if (!forumId) {
- forumId = offlineReply.forumid;
- }
- if (!courseid) {
- courseid = offlineReply.courseid;
- $scope.courseid = courseid;
- }
-
- convertPromises.push($mmaModForumOffline.convertOfflineReplyToOnline(offlineReply)
- .then(function(reply) {
- 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;
- }));
- });
-
- return $q.all(convertPromises).then(function() {
- // Convert back to array.
- onlinePosts = Object.keys(posts).map(function (key) {return posts[key];});
- });
- });
- }
+ return $mmaModForumOffline.getDiscussionReplies(discussionId).then(function(replies) {
+ $scope.postHasOffline = !!replies.length;
+
+ var convertPromises = [];
+
+ // Index posts to allow quick access.
+ var posts = {};
+ angular.forEach(onlinePosts, function(post) {
+ posts[post.id] = post;
+ });
+
+ angular.forEach(replies, function(offlineReply) {
+ // If we don't have forumId and courseId, get it from the post.
+ if (!forumId) {
+ forumId = offlineReply.forumid;
+ }
+ if (!courseId) {
+ courseId = offlineReply.courseid;
+ $scope.courseId = courseId;
+ }
+
+ convertPromises.push($mmaModForumHelper.convertOfflineReplyToOnline(offlineReply).then(function(reply) {
+ 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;
+ }));
+ });
+
+ return $q.all(convertPromises).then(function() {
+ // Convert back to array.
+ onlinePosts = Object.keys(posts).map(function (key) {return posts[key];});
+ });
});
});
}).then(function() {
@@ -149,7 +151,7 @@ angular.module('mm.addons.mod_forum')
$scope.posts = $mmaModForum.sortDiscussionPosts(posts, $scope.sort.direction);
}
$scope.defaultSubject = $translate.instant('mma.mod_forum.re') + ' ' + $scope.discussion.subject;
- $scope.newpost.subject = $scope.defaultSubject;
+ $scope.newPost.subject = $scope.defaultSubject;
// Now try to get the forum.
return fetchForum().then(function(forum) {
@@ -161,6 +163,7 @@ angular.module('mm.addons.mod_forum')
forumId = forum.id;
cmid = forum.cmid;
$scope.componentId = cmid;
+ $scope.forum = forum;
}).catch(function() {
// Ignore errors.
});
@@ -303,6 +306,23 @@ angular.module('mm.addons.mod_forum')
}
});
+ // Ask to confirm if there are changes.
+ function leaveView() {
+ var promise;
+
+ if (!$mmaModForumHelper.hasPostDataChanged($scope.newPost, $scope.originalData)) {
+ promise = $q.when();
+ } else {
+ // Show confirmation if some data has been modified.
+ promise = $mmUtil.showConfirm($translate('mm.core.confirmcanceledit'));
+ }
+
+ return promise.then(function() {
+ // Delete the local files from the tmp folder.
+ $mmaModForumHelper.clearTmpFiles($scope.newPost.files);
+ });
+ }
+
function scrollTop() {
if (!scrollView) {
scrollView = $ionicScrollDelegate.$getByHandle('mmaModForumPostsScroll');
@@ -312,12 +332,6 @@ angular.module('mm.addons.mod_forum')
// New post added.
$scope.postListChanged = function() {
- $scope.newpost.replyingto = undefined;
- $scope.newpost.editing = undefined;
- $scope.newpost.subject = $scope.defaultSubject;
- $scope.newpost.text = '';
- $scope.newpost.isEditing = false;
-
notifyPostListChanged();
$scope.discussionLoaded = false;
diff --git a/www/addons/mod/forum/controllers/newdiscussion.js b/www/addons/mod/forum/controllers/newdiscussion.js
index 7d2f00653d9..678de7e4262 100644
--- a/www/addons/mod/forum/controllers/newdiscussion.js
+++ b/www/addons/mod/forum/controllers/newdiscussion.js
@@ -23,31 +23,37 @@ angular.module('mm.addons.mod_forum')
*/
.controller('mmaModForumNewDiscussionCtrl', function($scope, $stateParams, $mmGroups, $q, $mmaModForum, $mmEvents, $ionicPlatform,
$mmUtil, $ionicHistory, $translate, mmaModForumNewDiscussionEvent, $mmaModForumOffline, $mmSite, mmaModForumComponent,
- mmaModForumAutomSyncedEvent, $mmSyncBlock, $mmaModForumSync, $mmText) {
+ mmaModForumAutomSyncedEvent, $mmSyncBlock, $mmaModForumSync, $mmText, $mmaModForumHelper) {
- var courseid = $stateParams.cid,
- forumid = $stateParams.forumid,
- cmid = $stateParams.cmid,
+ var courseId = $stateParams.cid,
+ forumId = $stateParams.forumid,
+ cmId = $stateParams.cmid,
timecreated = $stateParams.timecreated,
- forumName,
syncObserver,
- syncId;
+ syncId,
+ originalData;
- $scope.newdiscussion = {
+ // Block leaving the view, we want to show a confirm to the user if there's unsaved data.
+ $mmUtil.blockLeaveView($scope, leaveView);
+
+ $scope.newDiscussion = {
subject: '',
text: '',
- subscribe: true
+ subscribe: true,
+ files: []
};
$scope.hasOffline = false;
+ $scope.component = mmaModForumComponent;
+ $scope.canAddAttachments = $mmaModForum.canAddAttachments();
// Fetch if forum uses groups and the groups it uses.
function fetchDiscussionData(refresh) {
- return $mmGroups.getActivityGroupMode(cmid).then(function(mode) {
+ return $mmGroups.getActivityGroupMode(cmId).then(function(mode) {
var promises = [];
if (mode === $mmGroups.SEPARATEGROUPS || mode === $mmGroups.VISIBLEGROUPS) {
- promises.push($mmGroups.getActivityAllowedGroups(cmid).then(function(forumgroups) {
+ promises.push($mmGroups.getActivityAllowedGroups(cmId).then(function(forumgroups) {
var promise;
if (mode === $mmGroups.VISIBLEGROUPS) {
// We need to check which of the returned groups the user can post to.
@@ -61,8 +67,8 @@ angular.module('mm.addons.mod_forum')
if (forumgroups.length > 0) {
$scope.groups = forumgroups;
// Do not override groupid.
- $scope.newdiscussion.groupid = $scope.newdiscussion.groupid ?
- $scope.newdiscussion.groupid : forumgroups[0].id;
+ $scope.newDiscussion.groupid = $scope.newDiscussion.groupid ?
+ $scope.newDiscussion.groupid : forumgroups[0].id;
$scope.showGroups = true;
} else {
var message = mode === $mmGroups.SEPARATEGROUPS ?
@@ -75,32 +81,42 @@ angular.module('mm.addons.mod_forum')
$scope.showGroups = false;
}
- // Get forum name to send offline discussions.
- promises.push($mmaModForum.getForum(courseid, cmid).then(function(forum) {
- forumName = forum.name;
- }).catch(function() {
- // Ignore errors.
+ // Get forum.
+ promises.push($mmaModForum.getForum(courseId, cmId).then(function(forum) {
+ $scope.forum = forum;
}));
// If editing a discussion, get offline data.
if (timecreated && !refresh) {
- syncId = $mmaModForumSync.getForumSyncId(forumid);
+ syncId = $mmaModForumSync.getForumSyncId(forumId);
promises.push($mmaModForumSync.waitForSync(syncId).then(function() {
// Do not block if the scope is already destroyed.
if (!$scope.$$destroyed) {
$mmSyncBlock.blockOperation(mmaModForumComponent, syncId);
}
- return $mmaModForumOffline.getNewDiscussion(forumid, timecreated).then(function(discussion) {
+ return $mmaModForumOffline.getNewDiscussion(forumId, timecreated).then(function(discussion) {
$scope.hasOffline = true;
- $scope.newdiscussion.groupid = discussion.groupid ? discussion.groupid : $scope.newdiscussion.groupid;
- $scope.newdiscussion.subject = discussion.subject;
- $scope.newdiscussion.text = discussion.message;
- $scope.newdiscussion.subscribe = discussion.subscribe;
+ $scope.newDiscussion.groupid = discussion.groupid ? discussion.groupid : $scope.newDiscussion.groupid;
+ $scope.newDiscussion.subject = discussion.subject;
+ $scope.newDiscussion.text = discussion.message;
+ $scope.newDiscussion.subscribe = discussion.subscribe;
+
+ // Treat offline attachments if any.
+ if (discussion.attachments && discussion.attachments.offline) {
+ return $mmaModForumHelper.getNewDiscussionStoredFiles(forumId, timecreated).then(function(files) {
+ $scope.newDiscussion.files = files;
+ });
+ }
});
}));
}
+
return $q.all(promises);
- }).then(function(message) {
+ }).then(function() {
+ if (!originalData) {
+ // Initialize original data.
+ originalData = angular.copy($scope.newDiscussion);
+ }
$scope.showForm = true;
}).catch(function(message) {
$mmUtil.showErrorModalDefault(message, 'mma.mod_forum.errorgetgroups', true);
@@ -114,7 +130,7 @@ angular.module('mm.addons.mod_forum')
if ($mmaModForum.isCanAddDiscussionAvailable()) {
// Use the canAddDiscussion function to filter the groups.
// We first check if the user can post to all the groups.
- return $mmaModForum.canAddDiscussionToAll(forumid).catch(function() {
+ return $mmaModForum.canAddDiscussionToAll(forumId).catch(function() {
// The call failed, let's assume he can't.
return false;
}).then(function(canAdd) {
@@ -127,7 +143,7 @@ angular.module('mm.addons.mod_forum')
filtered = [];
angular.forEach(forumgroups, function(group) {
- promises.push($mmaModForum.canAddDiscussion(forumid, group.id).catch(function() {
+ promises.push($mmaModForum.canAddDiscussion(forumId, group.id).catch(function() {
// 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 true;
@@ -146,7 +162,7 @@ angular.module('mm.addons.mod_forum')
} else {
// We can't check it using WS. We'll get the groups the user belongs to and use them to
// filter the groups to post.
- return $mmGroups.getUserGroupsInCourse(courseid, refresh).then(function(usergroups) {
+ return $mmGroups.getUserGroupsInCourse(courseId, refresh).then(function(usergroups) {
if (usergroups.length === 0) {
// User doesn't belong to any group, probably a teacher. Let's return all groups,
// if the user can't post to some of them it will be filtered by add discussion WS.
@@ -179,9 +195,9 @@ angular.module('mm.addons.mod_forum')
// Pull to refresh.
$scope.refreshGroups = function() {
- var p1 = $mmGroups.invalidateActivityGroupMode(cmid),
- p2 = $mmGroups.invalidateActivityAllowedGroups(cmid),
- p3 = $mmaModForum.invalidateCanAddDiscussion(forumid);
+ var p1 = $mmGroups.invalidateActivityGroupMode(cmId),
+ p2 = $mmGroups.invalidateActivityAllowedGroups(cmId),
+ p3 = $mmaModForum.invalidateCanAddDiscussion(forumId);
$q.all([p1, p2, p3]).finally(function() {
fetchDiscussionData(true).finally(function() {
@@ -191,34 +207,61 @@ angular.module('mm.addons.mod_forum')
};
// Convenience function to update or return to discussions depending on device.
- function returnToDiscussions(discussionid) {
+ function returnToDiscussions(discussionId) {
var data = {
- forumid: forumid,
- cmid: cmid
+ forumid: forumId,
+ cmid: cmId
};
- if (discussionid) {
- data.discussionid = discussionid;
+ if (discussionId) {
+ data.discussionid = discussionId;
}
$mmEvents.trigger(mmaModForumNewDiscussionEvent, data);
+ // Delete the local files from the tmp folder.
+ $mmaModForumHelper.clearTmpFiles($scope.newDiscussion.files);
+
if ($ionicPlatform.isTablet()) {
// Empty form.
$scope.hasOffline = false;
- $scope.newdiscussion.subject = '';
- $scope.newdiscussion.text = '';
+ $scope.newDiscussion.subject = '';
+ $scope.newDiscussion.text = '';
+ $scope.newDiscussion.files = [];
+ originalData = angular.copy($scope.newDiscussion);
} else {
// Go back to discussions list.
$ionicHistory.goBack();
}
}
+ // Ask to confirm if there are changes.
+ function leaveView() {
+ var promise;
+
+ if (!$mmaModForumHelper.hasPostDataChanged($scope.newDiscussion, originalData)) {
+ promise = $q.when();
+ } else {
+ // Show confirmation if some data has been modified.
+ promise = $mmUtil.showConfirm($translate('mm.core.confirmcanceledit'));
+ }
+
+ return promise.then(function() {
+ // Delete the local files from the tmp folder.
+ $mmaModForumHelper.clearTmpFiles($scope.newDiscussion.files);
+ });
+ }
+
// Add a new discussion.
$scope.add = function() {
- var subject = $scope.newdiscussion.subject,
- message = $scope.newdiscussion.text,
- subscribe = $scope.newdiscussion.subscribe,
- groupid = $scope.newdiscussion.groupid;
+ var modal,
+ forumName = $scope.forum.name,
+ subject = $scope.newDiscussion.subject,
+ message = $scope.newDiscussion.text,
+ subscribe = $scope.newDiscussion.subscribe,
+ groupId = $scope.newDiscussion.groupid,
+ attachments = $scope.newDiscussion.files,
+ discTimecreated = timecreated || Date.now(),
+ saveOffline = false;
if (!subject) {
$mmUtil.showErrorModal('mma.mod_forum.erroremptysubject', true);
@@ -229,6 +272,8 @@ angular.module('mm.addons.mod_forum')
return;
}
+ modal = $mmUtil.showModalLoading('mm.core.sending', true);
+
// Check if rich text editor is enabled or not.
$mmUtil.isRichTextEditorEnabled().then(function(enabled) {
if (!enabled) {
@@ -236,19 +281,46 @@ angular.module('mm.addons.mod_forum')
message = $mmText.formatHtmlLines(message);
}
- return $mmaModForum.addNewDiscussion(forumid, forumName, courseid, subject, message, subscribe, groupid, undefined,
- timecreated);
- }).then(function(discussionid) {
- returnToDiscussions(discussionid);
+ // Upload attachments first if any.
+ if (attachments.length) {
+ return $mmaModForumHelper.uploadOrStoreNewDiscussionFiles(forumId, discTimecreated, attachments, false)
+ .catch(function() {
+ // Cannot upload them in online, save them in offline.
+ saveOffline = true;
+ return $mmaModForumHelper.uploadOrStoreNewDiscussionFiles(forumId, discTimecreated, attachments, true);
+ });
+ }
+ }).then(function(attach) {
+ if (saveOffline) {
+ // Save discussion in offline.
+ return $mmaModForumOffline.addNewDiscussion(forumId, forumName, courseId, subject,
+ message, subscribe, groupId, attach, discTimecreated).then(function() {
+ // Don't return anything.
+ });
+ } else {
+ // Try to send it to server.
+ // Don't allow offline if there are attachments since they were uploaded fine.
+ return $mmaModForum.addNewDiscussion(forumId, forumName, courseId, subject, message, subscribe,
+ groupId, attach, undefined, discTimecreated, !attachments.length);
+ }
+ }).then(function(discussionId) {
+ if (discussionId) {
+ // Data sent to server, delete stored files (if any).
+ $mmaModForumHelper.deleteNewDiscussionStoredFiles(forumId, discTimecreated);
+ }
+
+ returnToDiscussions(discussionId);
}).catch(function(message) {
$mmUtil.showErrorModalDefault(message, 'mma.mod_forum.cannotcreatediscussion', true);
+ }).finally(function() {
+ modal.dismiss();
});
};
if (timecreated) {
// Refresh data if this forum is synchronized automatically. Only if we're editing one.
syncObserver = $mmEvents.on(mmaModForumAutomSyncedEvent, function(data) {
- if (data && data.siteid == $mmSite.getId() && data.forumid == forumid && data.userid == $mmSite.getUserId()) {
+ if (data && data.siteid == $mmSite.getId() && data.forumid == forumId && data.userid == $mmSite.getUserId()) {
$mmUtil.showModal('mm.core.notice', 'mm.core.contenteditingsynced');
returnToDiscussions();
}
@@ -258,12 +330,26 @@ angular.module('mm.addons.mod_forum')
// Discard an offline saved discussion.
$scope.discard = function() {
return $mmUtil.showConfirm($translate('mm.core.areyousure')).then(function() {
- return $mmaModForumOffline.deleteNewDiscussion(forumid, timecreated).then(function() {
+ var promises = [];
+
+ promises.push($mmaModForumOffline.deleteNewDiscussion(forumId, timecreated));
+ promises.push($mmaModForumHelper.deleteNewDiscussionStoredFiles(forumId, timecreated).catch(function() {
+ // Ignore errors, maybe there are no files.
+ }));
+
+ return $q.all(promises).then(function() {
returnToDiscussions();
});
});
};
+ // Text changed when rendered.
+ $scope.firstRender = function() {
+ if (originalData) {
+ originalData.text = $scope.newDiscussion.text;
+ }
+ };
+
$scope.$on('$destroy', function(){
syncObserver && syncObserver.off && syncObserver.off();
if (syncId) {
diff --git a/www/addons/mod/forum/directives/discussionpost.js b/www/addons/mod/forum/directives/discussionpost.js
index 8a2d87ab172..a95b545443f 100644
--- a/www/addons/mod/forum/directives/discussionpost.js
+++ b/www/addons/mod/forum/directives/discussionpost.js
@@ -34,22 +34,44 @@ angular.module('mm.addons.mod_forum')
* @param {Boolean} showdivider True if it should have a list divider before the post.
* @param {Boolean} titleimportant True if title should be "important" (bold).
* @param {Boolean} unread True if post is being tracked and its not read.
+ * @param {Object} [forum] The forum the post belongs to. Required for attachments and offline posts.
* @param {Function} [onpostchange] Function to call when a post is added, updated or discarded.
* @param {String} [defaultsubject] Default subject to set to new posts.
* @param {String} [scrollHandle] Name of the scroll handle of the page containing the post.
+ * @param {Object} [originalData] Original newpost data. Used to detect if data has changed.
*/
.directive('mmaModForumDiscussionPost', function($mmaModForum, $mmUtil, $translate, $q, $mmaModForumOffline, $mmSyncBlock,
- mmaModForumComponent, $mmaModForumSync, $mmText) {
+ mmaModForumComponent, $mmaModForumSync, $mmText, $mmaModForumHelper, $ionicScrollDelegate) {
- // Get a forum. Returns empty object if params aren't valid.
- function getForum(courseId, cmId) {
- if (courseId && cmId) {
- return $mmaModForum.getForum(courseId, cmId);
+ // Confirm discard changes if any.
+ function confirmDiscard(scope) {
+ if (!$mmaModForumHelper.hasPostDataChanged(scope.newpost, scope.originalData)) {
+ return $q.when();
} else {
- return $q.when({}); // Return empty object.
+ // Show confirmation if some data has been modified.
+ return $mmUtil.showConfirm($translate('mm.core.confirmloss'));
}
}
+ // Set data to new post, clearing tmp files and updating original data.
+ function setPostData(scope, scrollView, replyingTo, editing, isEditing, subject, text, files) {
+ // Delete the local files from the tmp folder if any.
+ $mmaModForumHelper.clearTmpFiles(scope.newpost.files);
+
+ scope.newpost.replyingto = replyingTo;
+ scope.newpost.editing = editing;
+ scope.newpost.isEditing = !!isEditing;
+ scope.newpost.subject = subject || scope.defaultsubject || '';
+ scope.newpost.text = text || '';
+ scope.newpost.files = files || [];
+
+ // Update original data.
+ $mmUtil.copyProperties(scope.newpost, scope.originalData);
+
+ // Resize the scroll, some elements might have appeared or disappeared.
+ scrollView && scrollView.resize();
+ }
+
return {
restrict: 'E',
scope: {
@@ -64,38 +86,55 @@ angular.module('mm.addons.mod_forum')
showdivider: '=?',
titleimportant: '=?',
unread: '=?',
+ forum: '=?',
onpostchange: '&?',
defaultsubject: '=?',
- scrollHandle: '@?'
+ scrollHandle: '@?',
+ originalData: '=?'
},
templateUrl: 'addons/mod/forum/templates/discussionpost.html',
transclude: true,
link: function(scope) {
- var syncId;
+ var syncId,
+ scrollView = $ionicScrollDelegate.$getByHandle(scope.scrollHandle);
scope.isReplyEnabled = $mmaModForum.isReplyPostEnabled();
+ scope.canAddAttachments = $mmaModForum.canAddAttachments();
scope.uniqueid = scope.post.id ? 'reply' + scope.post.id : 'edit' + scope.post.parent;
// Set this post as being replied to.
scope.showReply = function() {
- scope.newpost.replyingto = scope.post.id;
- scope.newpost.editing = 'reply' + scope.post.id;
- scope.newpost.isEditing = false;
- scope.newpost.subject = scope.defaultsubject || '';
- scope.newpost.text = '';
+ var uniqueId = 'reply' + scope.post.id,
+ wasReplying = typeof scope.newpost.replyingto != 'undefined';
+
+ if (scope.newpost.isEditing) {
+ // User is editing a post, data needs to be resetted. Ask confirm if there is unsaved data.
+ confirmDiscard(scope).then(function() {
+ setPostData(scope, scrollView, scope.post.id, uniqueId, false);
+ });
+ } else if (!wasReplying) {
+ // User isn't replying, it's a brand new reply. Initialize the data.
+ setPostData(scope, scrollView, scope.post.id, uniqueId, false);
+ } else {
+ // The post being replied has changed but the data will be kept.
+ scope.newpost.replyingto = scope.post.id;
+ scope.newpost.editing = 'reply' + scope.post.id;
+ }
};
// Set this post as being edited to.
scope.editReply = function() {
- syncId = $mmaModForumSync.getDiscussionSyncId(scope.discussionId);
- $mmSyncBlock.blockOperation(mmaModForumComponent, syncId);
-
- scope.newpost.replyingto = scope.post.parent;
- scope.newpost.editing = 'edit' + scope.post.parent;
- scope.newpost.isEditing = true;
- scope.newpost.subject = scope.post.subject;
- scope.newpost.text = scope.post.message;
+ // Ask confirm if there is unsaved data.
+ confirmDiscard(scope).then(function() {
+ var uniqueId = 'edit' + scope.post.parent;
+
+ syncId = $mmaModForumSync.getDiscussionSyncId(scope.discussionId);
+ $mmSyncBlock.blockOperation(mmaModForumComponent, syncId);
+
+ setPostData(scope, scrollView, scope.post.parent, uniqueId, true, scope.post.subject,
+ scope.post.message, scope.post.attachments);
+ });
};
// Reply to this post.
@@ -109,8 +148,13 @@ angular.module('mm.addons.mod_forum')
return;
}
- var message = scope.newpost.text,
- modal = $mmUtil.showModalLoading('mm.core.sending', true);
+ var forum = scope.forum || {}, // Use empty object if forum isn't defined.
+ subject = scope.newpost.subject,
+ message = scope.newpost.text,
+ replyingTo = scope.newpost.replyingto,
+ files = scope.newpost.files || [],
+ modal = $mmUtil.showModalLoading('mm.core.sending', true),
+ saveOffline = false;
// Check if rich text editor is enabled or not.
$mmUtil.isRichTextEditorEnabled().then(function(enabled) {
@@ -119,63 +163,102 @@ angular.module('mm.addons.mod_forum')
message = message = $mmText.formatHtmlLines(message);
}
- return getForum(scope.courseid, scope.componentId).then(function(forum) {
- return $mmaModForum.replyPost(scope.newpost.replyingto, scope.discussionId, forum.id, forum.name,
- scope.courseid, scope.newpost.subject, message).then(function() {
- if (scope.onpostchange) {
- scope.onpostchange();
+ // Upload attachments first if any.
+ if (files.length) {
+ return $mmaModForumHelper.uploadOrStoreReplyFiles(forum.id, replyingTo, files, false).catch(function(err) {
+ // Cannot upload them in online, save them in offline.
+ if (!forum.id) {
+ // Cannot store them in offline without the forum ID. Reject.
+ return $q.reject(err);
}
+
+ saveOffline = true;
+ return $mmaModForumHelper.uploadOrStoreReplyFiles(forum.id, replyingTo, files, true);
});
- }).catch(function(message) {
- $mmUtil.showErrorModalDefault(message, 'mma.mod_forum.couldnotadd', true);
- });
- }).finally(function() {
- modal.dismiss();
+ }
+ }).then(function(attach) {
+ if (saveOffline) {
+ // Save post in offline.
+ return $mmaModForumOffline.replyPost(replyingTo, scope.discussionId, forum.id, forum.name,
+ scope.courseid, subject, message, attach).then(function() {
+ // Return false since it wasn't sent to server.
+ return false;
+ });
+ } else {
+ // Try to send it to server.
+ // Don't allow offline if there are attachments since they were uploaded fine.
+ return $mmaModForum.replyPost(replyingTo, scope.discussionId, forum.id, forum.name,
+ scope.courseid, subject, message, attach, undefined, !files.length);
+ }
+ }).then(function(sent) {
+ if (sent && forum.id) {
+ // Data sent to server, delete stored files (if any).
+ $mmaModForumHelper.deleteReplyStoredFiles(forum.id, replyingTo);
+ }
+
+ // Reset data.
+ setPostData(scope, scrollView);
+
+ if (scope.onpostchange) {
+ scope.onpostchange();
+ }
+
if (syncId) {
$mmSyncBlock.unblockOperation(mmaModForumComponent, syncId);
}
+ }).catch(function(message) {
+ $mmUtil.showErrorModalDefault(message, 'mma.mod_forum.couldnotadd', true);
+ }).finally(function() {
+ modal.dismiss();
});
};
// Cancel reply.
scope.cancel = function() {
- var promise;
- if ((!scope.newpost.subject || scope.newpost.subject == scope.defaultsubject) && !scope.newpost.text) {
- promise = $q.when(); // Nothing written, cancel right away.
- } else {
- promise = $mmUtil.showConfirm($translate('mm.core.areyousure'));
- }
+ confirmDiscard(scope).then(function() {
+ // Reset data.
+ setPostData(scope, scrollView);
- promise.then(function() {
- scope.newpost.replyingto = undefined;
- scope.newpost.editing = undefined;
- scope.newpost.subject = scope.defaultsubject || '';
- scope.newpost.text = '';
- scope.newpost.isEditing = false;
+ if (syncId) {
+ $mmSyncBlock.unblockOperation(mmaModForumComponent, syncId);
+ }
});
-
- if (syncId) {
- $mmSyncBlock.unblockOperation(mmaModForumComponent, syncId);
- }
};
// Discard reply.
scope.discard = function() {
$mmUtil.showConfirm($translate('mm.core.areyousure')).then(function() {
- return $mmaModForumOffline.deleteReply(scope.post.parent).finally(function() {
- scope.newpost.replyingto = undefined;
- scope.newpost.editing = undefined;
- scope.newpost.subject = scope.defaultsubject || '';
- scope.newpost.text = '';
- scope.newpost.isEditing = false;
+ var promises = [],
+ forum = scope.forum || {}; // Use empty object if forum isn't defined.
+
+ promises.push($mmaModForumOffline.deleteReply(scope.post.parent));
+ if (forum.id) {
+ promises.push($mmaModForumHelper.deleteReplyStoredFiles(forum.id, scope.post.parent).catch(function() {
+ // Ignore errors, maybe there are no files.
+ }));
+ }
+
+ return $q.all(promises).finally(function() {
+ // Reset data.
+ setPostData(scope, scrollView);
+
if (scope.onpostchange) {
scope.onpostchange();
}
+
+ if (syncId) {
+ $mmSyncBlock.unblockOperation(mmaModForumComponent, syncId);
+ }
});
});
- if (syncId) {
- $mmSyncBlock.unblockOperation(mmaModForumComponent, syncId);
+ };
+
+ // Text changed when rendered.
+ scope.firstRender = function() {
+ if (scope.newpost.isEditing) {
+ // Update original data.
+ $mmUtil.copyProperties(scope.newpost, scope.originalData);
}
};
diff --git a/www/addons/mod/forum/services/forum.js b/www/addons/mod/forum/services/forum.js
index dd26b270309..ecd775ae540 100644
--- a/www/addons/mod/forum/services/forum.js
+++ b/www/addons/mod/forum/services/forum.js
@@ -89,39 +89,43 @@ angular.module('mm.addons.mod_forum')
* @param {String} message New discussion's message.
* @param {String} subscribe True if should subscribe to the discussion, false otherwise.
* @param {String} [groupId] Group this discussion belongs to.
+ * @param {Mixed} [attach] The attachments ID if sending online, result of $mmFileUploader#storeFilesToUpload otherwise.
* @param {String} [siteId] Site ID. If not defined, current site.
* @param {Number} [timecreated] The time the discussion was created. Only used when editing discussion.
- * @return {Promise} Promise resolved when the discussion is created.
+ * @param {Boolean} allowOffline True if it can be stored in offline, false otherwise.
+ * @return {Promise} Promise resolved with discussion ID if sent online, resolved with false if stored offline.
*/
- self.addNewDiscussion = function(forumId, name, courseId, subject, message, subscribe, groupId, siteId, timecreated) {
+ self.addNewDiscussion = function(forumId, name, courseId, subject, message, subscribe, groupId, attach, siteId,
+ timecreated, allowOffline) {
siteId = siteId || $mmSite.getId();
// If we are editing an offline discussion, discard previous first.
var discardPromise = timecreated ? $mmaModForumOffline.deleteNewDiscussion(forumId, timecreated, siteId) : $q.when();
return discardPromise.then(function() {
- if (!$mmApp.isOnline()) {
+ if (!$mmApp.isOnline() && allowOffline) {
// App is offline, store the action.
return storeOffline();
}
- return self.addNewDiscussionOnline(forumId, subject, message, subscribe, groupId, siteId).then(function() {
- return true;
+ return self.addNewDiscussionOnline(forumId, subject, message, subscribe, groupId, attach, siteId).then(function(id) {
+ // Success, return the discussion ID.
+ return id;
}).catch(function(error) {
- if (error && error.wserror) {
- // The WebService has thrown an error, this means that responses cannot be deleted.
- return $q.reject(error.error);
- } else {
+ if (allowOffline && error && !error.wserror) {
// Couldn't connect to server, store in offline.
return storeOffline();
+ } else {
+ // The WebService has thrown an error or offline not supported, reject.
+ return $q.reject(error.error);
}
});
});
// Convenience function to store a message to be synchronized later.
function storeOffline() {
- return $mmaModForumOffline.addNewDiscussion(forumId, name, courseId, subject, message, subscribe, groupId, siteId)
- .then(function() {
+ return $mmaModForumOffline.addNewDiscussion(forumId, name, courseId, subject, message, subscribe,
+ groupId, attach, timecreated, siteId).then(function() {
return false;
});
}
@@ -133,15 +137,16 @@ angular.module('mm.addons.mod_forum')
* @module mm.addons.mod_forum
* @ngdoc method
* @name $mmaModForum#addNewDiscussionOnline
- * @param {Number} forumId Forum ID.
- * @param {String} subject New discussion's subject.
- * @param {String} message New discussion's message.
- * @param {String} subscribe True if should subscribe to the discussion, false otherwise.
- * @param {String} [groupId] Group this discussion belongs to.
- * @param {String} [siteId] Site ID. If not defined, current site.
- * @return {Promise} Promise resolved when the discussion is created.
- */
- self.addNewDiscussionOnline = function(forumId, subject, message, subscribe, groupId, siteId) {
+ * @param {Number} forumId Forum ID.
+ * @param {String} subject New discussion's subject.
+ * @param {String} message New discussion's message.
+ * @param {String} subscribe True if should subscribe to the discussion, false otherwise.
+ * @param {String} [groupId] Group this discussion belongs to.
+ * @param {Number} [attachId] Attachments ID (if any attachment).
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the discussion is created.
+ */
+ self.addNewDiscussionOnline = function(forumId, subject, message, subscribe, groupId, attachId, siteId) {
siteId = siteId || $mmSite.getId();
return $mmSitesManager.getSite(siteId).then(function(site) {
@@ -156,10 +161,18 @@ angular.module('mm.addons.mod_forum')
}
]
};
+
if (groupId) {
params.groupid = groupId;
}
+ if (attachId) {
+ params.options.push({
+ name: 'attachmentsid',
+ value: attachId
+ });
+ }
+
return site.write('mod_forum_add_discussion', params).catch(function(error) {
return $q.reject({
error: error,
@@ -178,6 +191,23 @@ angular.module('mm.addons.mod_forum')
});
};
+ /**
+ * Check if a the site allows adding attachments in posts and discussions.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForum#canAddAttachments
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with a boolean: true if can add attachments, false otherwise.
+ */
+ self.canAddAttachments = function(siteId) {
+ return $mmSitesManager.getSite(siteId).then(function(site) {
+ // Attachments allowed from Moodle 3.1.
+ var version = parseInt(site.getInfo().version, 10);
+ return version && version >= 2016052300;
+ });
+ };
+
/**
* Check if a user can post to a certain group.
*
@@ -701,35 +731,37 @@ angular.module('mm.addons.mod_forum')
* @module mm.addons.mod_forum
* @ngdoc method
* @name $mmaModForum#replyPost
- * @param {Number} postId ID of the post being replied.
- * @param {Number} discussionId ID of the discussion the user is replying to.
- * @param {Number} forumId ID of the forum the user is replying to.
- * @param {String} name Forum name.
- * @param {Number} courseId Course ID the forum belongs to.
- * @param {String} subject New post's subject.
- * @param {String} message New post's message.
- * @param {String} [siteId] Site ID. If not defined, current site.
- * @return {Promise} Promise resolved when the post is created.
- */
- self.replyPost = function(postId, discussionId, forumId, name, courseId, subject, message, siteId) {
+ * @param {Number} postId ID of the post being replied.
+ * @param {Number} discussionId ID of the discussion the user is replying to.
+ * @param {Number} forumId ID of the forum the user is replying to.
+ * @param {String} name Forum name.
+ * @param {Number} courseId Course ID the forum belongs to.
+ * @param {String} subject New post's subject.
+ * @param {String} message New post's message.
+ * @param {Mixed} [attach] The attachments ID if sending online, result of $mmFileUploader#storeFilesToUpload otherwise.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Boolean} allowOffline True if it can be stored in offline, false otherwise.
+ * @return {Promise} Promise resolved when the post is created.
+ */
+ self.replyPost = function(postId, discussionId, forumId, name, courseId, subject, message, attach, siteId, allowOffline) {
siteId = siteId || $mmSite.getId();
- if (!$mmApp.isOnline()) {
+ if (!$mmApp.isOnline() && allowOffline) {
// App is offline, store the action.
return storeOffline();
}
// If there's already a reply to be sent to the server, discard it first.
return $mmaModForumOffline.deleteReply(postId, siteId).then(function() {
- return self.replyPostOnline(postId, subject, message, siteId).then(function() {
+ return self.replyPostOnline(postId, subject, message, attach, siteId).then(function() {
return true;
}).catch(function(error) {
- if (error && error.wserror) {
- // The WebService has thrown an error, this means that responses cannot be deleted.
- return $q.reject(error.error);
- } else {
+ if (allowOffline && error && !error.wserror) {
// Couldn't connect to server, store in offline.
return storeOffline();
+ } else {
+ // The WebService has thrown an error or offline not supported, reject.
+ return $q.reject(error.error);
}
});
});
@@ -741,7 +773,7 @@ angular.module('mm.addons.mod_forum')
return $mmLang.translateAndReject('mm.core.networkerrormsg');
}
- return $mmaModForumOffline.replyPost(postId, discussionId, forumId, name, courseId, subject, message, siteId)
+ return $mmaModForumOffline.replyPost(postId, discussionId, forumId, name, courseId, subject, message, attach, siteId)
.then(function() {
return false;
});
@@ -754,22 +786,31 @@ angular.module('mm.addons.mod_forum')
* @module mm.addons.mod_forum
* @ngdoc method
* @name $mmaModForum#replyPostOnline
- * @param {Number} postId ID of the post being replied.
- * @param {String} subject New post's subject.
- * @param {String} message New post's message.
- * @param {String} [siteId] Site ID. If not defined, current site.
- * @return {Promise} Promise resolved when the post is created.
- */
- self.replyPostOnline = function(postId, subject, message, siteId) {
+ * @param {Number} postId ID of the post being replied.
+ * @param {String} subject New post's subject.
+ * @param {String} message New post's message.
+ * @param {Number} [attachId] Attachments ID (if any attachment).
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the post is created.
+ */
+ self.replyPostOnline = function(postId, subject, message, attachId, siteId) {
siteId = siteId || $mmSite.getId();
return $mmSitesManager.getSite(siteId).then(function(site) {
var params = {
postid: postId,
subject: subject,
- message: message
+ message: message,
+ options: []
};
+ if (attachId) {
+ params.options.push({
+ name: 'attachmentsid',
+ value: attachId
+ });
+ }
+
return site.write('mod_forum_add_discussion_post', params).catch(function(error) {
return $q.reject({
error: error,
diff --git a/www/addons/mod/forum/services/forum_offline.js b/www/addons/mod/forum/services/forum_offline.js
index caa8acddd13..5386fb29821 100644
--- a/www/addons/mod/forum/services/forum_offline.js
+++ b/www/addons/mod/forum/services/forum_offline.js
@@ -88,7 +88,7 @@ angular.module('mm.addons.mod_forum')
* @name $mmaModForumOffline
*/
.factory('$mmaModForumOffline', function($log, mmaModForumOfflineDiscussionsStore, $mmSitesManager, mmaModForumOfflineRepliesStore,
- $mmSite, $mmUser) {
+ $mmSite, $mmFS) {
$log = $log.getInstance('$mmaModForumOffline');
@@ -199,18 +199,21 @@ angular.module('mm.addons.mod_forum')
* @module mm.addons.mod_forum
* @ngdoc method
* @name $mmaModForumOffline#addNewDiscussion
- * @param {Number} forumId Forum ID.
- * @param {String} name Forum name.
- * @param {Number} courseId Course ID the forum belongs to.
- * @param {String} subject New discussion's subject.
- * @param {String} message New discussion's message.
- * @param {String} subscribe True if should subscribe to the discussion, false otherwise.
- * @param {String} [groupId] Group this discussion belongs to.
- * @param {String} [siteId] Site ID. If not defined, current site.
- * @param {Number} [userId] User the discussion belong to. If not defined, current user in site.
- * @return {Promise} Promise resolved when new discussion is successfully saved.
+ * @param {Number} forumId Forum ID.
+ * @param {String} name Forum name.
+ * @param {Number} courseId Course ID the forum belongs to.
+ * @param {String} subject New discussion's subject.
+ * @param {String} message New discussion's message.
+ * @param {String} subscribe True if should subscribe to the discussion, false otherwise.
+ * @param {String} [groupId] Group this discussion belongs to.
+ * @param {Object} [attach] Result of $mmFileUploader#storeFilesToUpload for attachments.
+ * @param {Number} [timecreated] The time the discussion was created. If not defined, current time.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the discussion belong to. If not defined, current user in site.
+ * @return {Promise} Promise resolved when new discussion is successfully saved.
*/
- self.addNewDiscussion = function(forumId, name, courseId, subject, message, subscribe, groupId, siteId, userId) {
+ self.addNewDiscussion = function(forumId, name, courseId, subject, message, subscribe, groupId, attach, timecreated,
+ siteId, userId) {
siteId = siteId || $mmSite.getId();
return $mmSitesManager.getSite(siteId).then(function(site) {
@@ -226,8 +229,13 @@ angular.module('mm.addons.mod_forum')
subscribe: subscribe,
groupid: groupId || -1,
userid: userId,
- timecreated: new Date().getTime()
+ timecreated: timecreated || new Date().getTime()
};
+
+ if (attach) {
+ entry.attachments = attach;
+ }
+
return db.insert(mmaModForumOfflineDiscussionsStore, entry);
});
};
@@ -350,42 +358,60 @@ angular.module('mm.addons.mod_forum')
};
/**
- * Convert offline reply to online format in order to be compatible with them.
+ * Get the path to the folder where to store files for offline attachments in a forum.
*
* @module mm.addons.mod_forum
* @ngdoc method
- * @name $mmaModForumOffline#convertOfflineReplyToOnline
- * @param {Object} offlineReply Offline version of the reply.
- * @return {Promise} Promise resolved with the object converted to Online.
+ * @name $mmaModForumOffline#getForumFolder
+ * @param {Number} forumId Forum ID.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the path.
*/
- self.convertOfflineReplyToOnline = function(offlineReply) {
- var reply = {
- attachment: "",
- 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,
- subject: offlineReply.subject,
- totalscore: 0,
- userid: offlineReply.userid
- };
-
- return $mmUser.getProfile(offlineReply.userid, offlineReply.courseid, true).then(function(user) {
- reply.userfullname = user.fullname;
- reply.userpictureurl = user.profileimageurl;
- }).catch(function() {
- // Ignore errors.
- }).then(function() {
- return reply;
+ self.getForumFolder = function(forumId, siteId) {
+ return $mmSitesManager.getSite(siteId).then(function(site) {
+
+ var siteFolderPath = $mmFS.getSiteFolder(site.getId()),
+ forumFolderPath = 'offlineforum/' + forumId;
+
+ return $mmFS.concatenatePaths(siteFolderPath, forumFolderPath);
+ });
+ };
+
+ /**
+ * Get the path to the folder where to store files for a new offline discussion.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumOffline#getNewDiscussionFolder
+ * @param {Number} forumId Forum ID.
+ * @param {Number} timecreated The time the discussion was created.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the path.
+ */
+ self.getNewDiscussionFolder = function(forumId, timecreated, siteId) {
+ return self.getForumFolder(forumId, siteId).then(function(folderPath) {
+ return $mmFS.concatenatePaths(folderPath, 'newdisc_' + timecreated);
+ });
+ };
+
+ /**
+ * Get the path to the folder where to store files for a new offline reply.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumOffline#getReplyFolder
+ * @param {Number} forumId Forum ID.
+ * @param {Number} postId ID of the post being replied.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the replies belong to. If not defined, current user in site.
+ * @return {Promise} Promise resolved with the path.
+ */
+ self.getReplyFolder = function(forumId, postId, siteId, userId) {
+ return self.getForumFolder(forumId, siteId).then(function(folderPath) {
+ return $mmSitesManager.getSite(siteId).then(function(site) {
+ userId = userId || site.getUserId();
+ return $mmFS.concatenatePaths(folderPath, 'reply_' + postId + '_' + userId);
+ });
});
};
@@ -395,18 +421,19 @@ angular.module('mm.addons.mod_forum')
* @module mm.addons.mod_forum
* @ngdoc method
* @name $mmaModForumOffline#replyPost
- * @param {Number} postId ID of the post being replied.
- * @param {Number} discussionId ID of the discussion the user is replying to.
- * @param {Number} forumId ID of the forum the user is replying to.
- * @param {String} name Forum name.
- * @param {Number} courseId Course ID the forum belongs to.
- * @param {String} subject New post's subject.
- * @param {String} message New post's message.
- * @param {String} [siteId] Site ID. If not defined, current site.
- * @param {Number} [userId] User the post belong to. If not defined, current user in site.
- * @return {Promise} Promise resolved when the post is created.
+ * @param {Number} postId ID of the post being replied.
+ * @param {Number} discussionId ID of the discussion the user is replying to.
+ * @param {Number} forumId ID of the forum the user is replying to.
+ * @param {String} name Forum name.
+ * @param {Number} courseId Course ID the forum belongs to.
+ * @param {String} subject New post's subject.
+ * @param {String} message New post's message.
+ * @param {Object} [attach] Result of $mmFileUploader#storeFilesToUpload for attachments.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the post belong to. If not defined, current user in site.
+ * @return {Promise} Promise resolved when the post is created.
*/
- self.replyPost = function(postId, discussionId, forumId, name, courseId, subject, message, siteId, userId) {
+ self.replyPost = function(postId, discussionId, forumId, name, courseId, subject, message, attach, siteId, userId) {
siteId = siteId || $mmSite.getId();
return $mmSitesManager.getSite(siteId).then(function(site) {
@@ -424,6 +451,11 @@ angular.module('mm.addons.mod_forum')
userid: userId,
timecreated: new Date().getTime()
};
+
+ if (attach) {
+ discussion.attachments = attach;
+ }
+
return db.insert(mmaModForumOfflineRepliesStore, discussion);
});
};
diff --git a/www/addons/mod/forum/services/forum_sync.js b/www/addons/mod/forum/services/forum_sync.js
index a7fd01b0201..f57e5e068cf 100644
--- a/www/addons/mod/forum/services/forum_sync.js
+++ b/www/addons/mod/forum/services/forum_sync.js
@@ -22,7 +22,8 @@ angular.module('mm.addons.mod_forum')
* @name $mmaModForumSync
*/
.factory('$mmaModForumSync', function($q, $log, $mmApp, $mmSitesManager, $mmaModForumOffline, $mmSite, $mmEvents, $mmSync, $mmLang,
- mmaModForumComponent, $mmaModForum, $translate, mmaModForumAutomSyncedEvent, mmaModForumSyncTime, $mmCourse, $mmSyncBlock) {
+ mmaModForumComponent, $mmaModForum, $translate, mmaModForumAutomSyncedEvent, mmaModForumSyncTime, $mmCourse, $mmSyncBlock,
+ $mmaModForumHelper, $mmFileUploader) {
$log = $log.getInstance('$mmaModForumSync');
@@ -207,19 +208,22 @@ angular.module('mm.addons.mod_forum')
courseId = data.courseid;
- // A user has added some discussions.
- promise = $mmaModForum.addNewDiscussionOnline(forumId, data.subject, data.message, data.subscribe, data.groupid,
- siteId);
+ // First of all upload the attachments (if any).
+ promise = uploadAttachments(forumId, data, true, siteId, userId).then(function(itemId) {
+ // Now try to add the discussion.
+ return $mmaModForum.addNewDiscussionOnline(forumId, data.subject, data.message,
+ data.subscribe, data.groupid, itemId, siteId);
+ });
promises.push(promise.then(function() {
result.updated = true;
- return $mmaModForumOffline.deleteNewDiscussion(forumId, data.timecreated, siteId, userId);
+ return deleteNewDiscussion(forumId, data.timecreated, siteId, userId);
}).catch(function(error) {
if (error && error.wserror) {
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
result.updated = true;
- return $mmaModForumOffline.deleteNewDiscussion(forumId, data.timecreated, siteId, userId).then(function() {
+ return deleteNewDiscussion(forumId, data.timecreated, siteId, userId).then(function() {
// Responses deleted, add a warning.
result.warnings.push($translate.instant('mm.core.warningofflinedatadeleted', {
component: $mmCourse.translateModuleName('forum'),
@@ -259,6 +263,46 @@ angular.module('mm.addons.mod_forum')
return self.addOngoingSync(syncId, syncPromise, siteId);
};
+ /**
+ * Delete a new discussion.
+ *
+ * @param {Number} forumId Forum ID the discussion belongs to.
+ * @param {Number} timecreated The timecreated of the discussion.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the discussion belongs to. If not defined, current user in site.
+ * @return {Promise} Promise resolved when deleted.
+ */
+ function deleteNewDiscussion(forumId, timecreated, siteId, userId) {
+ var promises = [];
+
+ promises.push($mmaModForumOffline.deleteNewDiscussion(forumId, timecreated, siteId, userId));
+ promises.push($mmaModForumHelper.deleteNewDiscussionStoredFiles(forumId, timecreated, siteId).catch(function() {
+ // Ignore errors, maybe there are no files.
+ }));
+
+ return $q.all(promises);
+ }
+
+ /**
+ * Delete a new discussion.
+ *
+ * @param {Number} forumId Forum ID the discussion belongs to.
+ * @param {Number} postId ID of the post being replied.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the discussion belongs to. If not defined, current user in site.
+ * @return {Promise} Promise resolved when deleted.
+ */
+ function deleteReply(forumId, postId, siteId, userId) {
+ var promises = [];
+
+ promises.push($mmaModForumOffline.deleteReply(postId, siteId, userId));
+ promises.push($mmaModForumHelper.deleteReplyStoredFiles(forumId, postId, siteId, userId).catch(function() {
+ // Ignore errors, maybe there are no files.
+ }));
+
+ return $q.all(promises);
+ }
+
/**
* Synchronize all offline discussion replies of a forum.
*
@@ -390,18 +434,21 @@ angular.module('mm.addons.mod_forum')
courseId = data.courseid;
forumId = data.forumid;
- // A user has added some discussions.
- promise = $mmaModForum.replyPostOnline(data.postid, data.subject, data.message, siteId);
+ // First of all upload the attachments (if any).
+ promise = uploadAttachments(forumId, data, false, siteId, userId).then(function(itemId) {
+ // Now try to send the reply.
+ return $mmaModForum.replyPostOnline(data.postid, data.subject, data.message, itemId, siteId);
+ });
promises.push(promise.then(function() {
result.updated = true;
- return $mmaModForumOffline.deleteReply(data.postid, siteId, userId);
+ return deleteReply(forumId, data.postid, siteId, userId);
}).catch(function(error) {
if (error && error.wserror) {
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
result.updated = true;
- return $mmaModForumOffline.deleteReply(data.postid, siteId, userId).then(function() {
+ return deleteReply(forumId, data.postid, siteId, userId).then(function() {
// Responses deleted, add a warning.
result.warnings.push($translate.instant('mm.core.warningofflinedatadeleted', {
component: $mmCourse.translateModuleName('forum'),
@@ -441,6 +488,49 @@ angular.module('mm.addons.mod_forum')
return self.addOngoingSync(syncId, syncPromise, siteId);
};
+ /**
+ * Upload attachments of an offline post/discussion.
+ *
+ * @param {Number} forumId Forum ID the post belongs to.
+ * @param {Object} post Offline post or discussion.
+ * @param {Boolean} isDisc True if it's a new discussion, false if it's a reply.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the reply belongs to. If not defined, current user in site.
+ * @return {Promise} Promise resolved with draftid if uploaded, resolved with undefined if nothing to upload.
+ */
+ function uploadAttachments(forumId, post, isDisc, siteId, userId) {
+ var attachments = post && post.attachments;
+ if (attachments) {
+ // Has some attachments to sync.
+ var files = attachments.online || [],
+ promise;
+
+ if (attachments.offline) {
+ // Has offline files.
+ if (isDisc) {
+ promise = $mmaModForumHelper.getNewDiscussionStoredFiles(forumId, post.timecreated, siteId);
+ } else {
+ promise = $mmaModForumHelper.getReplyStoredFiles(forumId, post.postid, siteId, userId);
+ }
+
+ promise.then(function(atts) {
+ files = files.concat(atts);
+ }).catch(function() {
+ // Folder not found, no files to add.
+ });
+ } else {
+ promise = $q.when();
+ }
+
+ return promise.then(function() {
+ return $mmFileUploader.uploadOrReuploadFiles(files, mmaModForumComponent, forumId, siteId);
+ });
+ }
+
+ // No attachments, resolve.
+ return $q.when();
+ }
+
/**
* Get the ID of a forum sync.
*
diff --git a/www/addons/mod/forum/services/helper.js b/www/addons/mod/forum/services/helper.js
new file mode 100644
index 00000000000..6de9cc292bb
--- /dev/null
+++ b/www/addons/mod/forum/services/helper.js
@@ -0,0 +1,311 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+angular.module('mm.addons.mod_forum')
+
+/**
+ * Helper to gather some common functions for forum.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc service
+ * @name $mmaModForumHelper
+ */
+.factory('$mmaModForumHelper', function($mmaModForumOffline, $mmSite, $mmFileUploader, $mmFS, mmaModForumComponent, $mmUser, $q) {
+
+ var self = {};
+
+ /**
+ * Clear temporary attachments because a new discussion or post was cancelled.
+ * Attachments already saved in an offline discussion or post will NOT be deleted.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#clearTmpFiles
+ * @param {Object[]} files List of current files.
+ * @return {Void}
+ */
+ self.clearTmpFiles = function(files) {
+ // Delete the local files from the tmp folder.
+ files.forEach(function(file) {
+ if (!file.offline && file.remove) {
+ file.remove();
+ }
+ });
+ };
+
+ /**
+ * Convert offline reply to online format in order to be compatible with them.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#convertOfflineReplyToOnline
+ * @param {Object} offlineReply Offline version of the reply.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the object converted to Online.
+ */
+ self.convertOfflineReplyToOnline = function(offlineReply, siteId) {
+ var reply = {
+ 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,
+ subject: offlineReply.subject,
+ totalscore: 0,
+ userid: offlineReply.userid
+ },
+ promises = [];
+
+ // Treat attachments if any.
+ if (offlineReply.attachments) {
+ reply.attachments = offlineReply.attachments.online || [];
+
+ if (offlineReply.attachments.offline) {
+ promises.push(self.getReplyStoredFiles(offlineReply.forumid, reply.parent, siteId, reply.userid)
+ .then(function(files) {
+ reply.attachments = reply.attachments.concat(files);
+ }));
+ }
+ }
+
+ // Get user data.
+ promises.push($mmUser.getProfile(offlineReply.userid, offlineReply.courseid, true).then(function(user) {
+ reply.userfullname = user.fullname;
+ reply.userpictureurl = user.profileimageurl;
+ }).catch(function() {
+ // Ignore errors.
+ }));
+
+ return $q.all(promises).then(function() {
+ reply.attachment = reply.attachments.length > 0 ? 1 : 0;
+ return reply;
+ });
+ };
+
+ /**
+ * Delete stored attachment files for a new discussion.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#deleteNewDiscussionStoredFiles
+ * @param {Number} forumId Forum ID.
+ * @param {Number} timecreated The time the discussion was created.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when deleted.
+ */
+ self.deleteNewDiscussionStoredFiles = function(forumId, timecreated, siteId) {
+ return $mmaModForumOffline.getNewDiscussionFolder(forumId, timecreated, siteId).then(function(folderPath) {
+ return $mmFS.removeDir(folderPath);
+ });
+ };
+
+ /**
+ * Delete stored attachment files for a reply.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#deleteReplyStoredFiles
+ * @param {Number} forumId Forum ID.
+ * @param {Number} postId ID of the post being replied.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the reply belongs to. If not defined, current user in site.
+ * @return {Promise} Promise resolved when deleted.
+ */
+ self.deleteReplyStoredFiles = function(forumId, postId, siteId, userId) {
+ return $mmaModForumOffline.getReplyFolder(forumId, postId, siteId, userId).then(function(folderPath) {
+ return $mmFS.removeDir(folderPath);
+ });
+ };
+
+ /**
+ * Get a list of stored attachment files for a new discussion. See $mmaModForumHelper#storeNewDiscussionFiles.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#getNewDiscussionStoredFiles
+ * @param {Number} forumId Forum ID.
+ * @param {Number} timecreated The time the discussion was created.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the files.
+ */
+ self.getNewDiscussionStoredFiles = function(forumId, timecreated, siteId) {
+ return $mmaModForumOffline.getNewDiscussionFolder(forumId, timecreated, siteId).then(function(folderPath) {
+ return getStoredFiles(folderPath);
+ });
+ };
+
+ /**
+ * Get a list of stored attachment files for a reply. See $mmaModForumHelper#storeReplyFiles.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#getReplyStoredFiles
+ * @param {Number} forumId Forum ID.
+ * @param {Number} postId ID of the post being replied.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the reply belongs to. If not defined, current user in site.
+ * @return {Promise} Promise resolved with the files.
+ */
+ self.getReplyStoredFiles = function(forumId, postId, siteId, userId) {
+ return $mmaModForumOffline.getReplyFolder(forumId, postId, siteId, userId).then(function(folderPath) {
+ return getStoredFiles(folderPath);
+ });
+ };
+
+ /**
+ * Get the files stored in a folder, marking them as offline.
+ *
+ * @param {String} folderPath Folder where to get the files.
+ * @return {Promise} Promise resolved with the list of files.
+ */
+ function getStoredFiles(folderPath) {
+ return $mmFS.getDirectoryContents(folderPath).then(function(files) {
+ // Mark the files as pending offline.
+ angular.forEach(files, function(file) {
+ file.offline = true;
+ file.filename = file.name;
+ });
+ return files;
+ });
+ }
+
+ /**
+ * Check if the data of a post/discussion has changed.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#hasPostDataChanged
+ * @param {Object} post Current data.
+ * @param {Object} original Original data.
+ * @return {Boolean} True if data has changed, false otherwise.
+ */
+ self.hasPostDataChanged = function(post, original) {
+ if (!original || typeof original.subject == 'undefined') {
+ // There is no original data, assume it hasn't changed.
+ return false;
+ }
+
+ var postFiles = post.files || [],
+ originalFiles = original.files || [];
+
+ if (original.subject != post.subject || original.text != post.text || postFiles.length != originalFiles.length) {
+ return true;
+ }
+
+ for (var i = 0; i < postFiles.length; i++) {
+ if (postFiles[i].name != originalFiles[i].name) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Given a list of files (either online files or local files), store the local files in a local folder
+ * to be submitted later.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#storeNewDiscussionFiles
+ * @param {Number} forumId Forum ID.
+ * @param {Number} timecreated The time the discussion was created.
+ * @param {Object[]} files List of files.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if success, rejected otherwise.
+ */
+ self.storeNewDiscussionFiles = function(forumId, timecreated, files, siteId) {
+ siteId = siteId || $mmSite.getId();
+
+ // Get the folder where to store the files.
+ return $mmaModForumOffline.getNewDiscussionFolder(forumId, timecreated, siteId).then(function(folderPath) {
+ return $mmFileUploader.storeFilesToUpload(folderPath, files);
+ });
+ };
+
+ /**
+ * Given a list of files (either online files or local files), store the local files in a local folder
+ * to be submitted later.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#storeReplyFiles
+ * @param {Number} forumId Forum ID.
+ * @param {Number} postId ID of the post being replied.
+ * @param {Object[]} files List of files.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the reply belongs to. If not defined, current user in site.
+ * @return {Promise} Promise resolved if success, rejected otherwise.
+ */
+ self.storeReplyFiles = function(forumId, postId, files, siteId, userId) {
+ // Get the folder where to store the files.
+ return $mmaModForumOffline.getReplyFolder(forumId, postId, siteId, userId).then(function(folderPath) {
+ return $mmFileUploader.storeFilesToUpload(folderPath, files);
+ });
+ };
+
+ /**
+ * Upload or store some files for a new discussion, depending if the user is offline or not.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#uploadOrStoreNewDiscussionFiles
+ * @param {Number} forumId Forum ID.
+ * @param {Number} timecreated The time the discussion was created.
+ * @param {Object[]} files List of files.
+ * @param {Boolean} offline True if files sould be stored for offline, false to upload them.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if success.
+ */
+ self.uploadOrStoreNewDiscussionFiles = function(forumId, timecreated, files, offline, siteId) {
+ if (offline) {
+ return self.storeNewDiscussionFiles(forumId, timecreated, files, siteId);
+ } else {
+ return $mmFileUploader.uploadOrReuploadFiles(files, mmaModForumComponent, forumId, siteId);
+ }
+ };
+
+ /**
+ * Upload or store some files for a reply, depending if the user is offline or not.
+ *
+ * @module mm.addons.mod_forum
+ * @ngdoc method
+ * @name $mmaModForumHelper#uploadOrStoreReplyFiles
+ * @param {Number} forumId Forum ID.
+ * @param {Number} postId ID of the post being replied.
+ * @param {Object[]} files List of files.
+ * @param {Boolean} offline True if files sould be stored for offline, false to upload them.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {Number} [userId] User the reply belongs to. If not defined, current user in site.
+ * @return {Promise} Promise resolved if success.
+ */
+ self.uploadOrStoreReplyFiles = function(forumId, postId, files, offline, siteId, userId) {
+ if (offline) {
+ return self.storeReplyFiles(forumId, postId, files, siteId, userId);
+ } else {
+ return $mmFileUploader.uploadOrReuploadFiles(files, mmaModForumComponent, forumId, siteId);
+ }
+ };
+
+ return self;
+});
diff --git a/www/addons/mod/forum/templates/discussion.html b/www/addons/mod/forum/templates/discussion.html
index 494f40ea54f..c5d84e6d89b 100644
--- a/www/addons/mod/forum/templates/discussion.html
+++ b/www/addons/mod/forum/templates/discussion.html
@@ -23,7 +23,7 @@