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 @@
- +
@@ -33,7 +33,7 @@