diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index ca8755442..f6f6591a6 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -1196,7 +1196,7 @@ img { border-color: #d6e9c6; } #messaging .warning { - color: #8a6d3b; + color: #725523; background-color: #fcf8e3; border-color: #faebcc; } diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index 7acdb78e6..fc60c3396 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -89,9 +89,15 @@ angular.module('OpenSlidesApp.core', [ } }; socket.onmessage = function (event) { - _.forEach(Autoupdate.messageReceivers, function (receiver) { - receiver(event.data); - }); + var dataList = []; + try { + dataList = JSON.parse(event.data); + _.forEach(Autoupdate.messageReceivers, function (receiver) { + receiver(dataList); + }); + } catch(err) { + console.error(err); + } // Check if the promise is not resolved yet. if (Autoupdate.firstMessageDeferred.promise.$$state.status === 0) { Autoupdate.firstMessageDeferred.resolve(); @@ -268,14 +274,7 @@ angular.module('OpenSlidesApp.core', [ 'dsEject', function (DS, autoupdate, dsEject) { // Handler for normal autoupdate messages. - autoupdate.onMessage(function(json) { - var dataList = []; - try { - dataList = JSON.parse(json); - } catch(err) { - console.error(json); - } - + autoupdate.onMessage(function(dataList) { var dataListByCollection = _.groupBy(dataList, 'collection'); _.forEach(dataListByCollection, function (list, key) { var changedElements = []; @@ -314,26 +313,108 @@ angular.module('OpenSlidesApp.core', [ }); }); }); + } +]) + +.factory('Notify', [ + 'autoupdate', + 'operator', + function (autoupdate, operator) { + var anonymousTrackId; // Handler for notify messages. - autoupdate.onMessage(function(json) { - var dataList = []; - try { - dataList = JSON.parse(json); - } catch(err) { - console.error(json); - } - + autoupdate.onMessage(function(dataList) { var dataListByCollection = _.groupBy(dataList, 'collection'); - _.forEach(dataListByCollection, function (list, key) { - _.forEach(list, function (data) { - if (data.collection === 'notify') { - // TODO: Add more code here. - console.log(data); + _.forEach(dataListByCollection.notify, function (notifyItem) { + // Check, if this current user (or anonymous instance) has send this notify. + if (notifyItem.senderUserId) { + if (operator.user) { // User send to user + notifyItem.sendBySelf = (notifyItem.senderUserId === operator.user.id); + } else { // User send to anonymous + notifyItem.sendBySelf = false; } + } else { + if (operator.user) { // Anonymous send to user + notifyItem.sendBySelf = false; + } else { // Anonymous send to anonymous + notifyItem.sendBySelf = (notifyItem.anonymousTrackId === anonymousTrackId); + } + } + // notify registered receivers. + _.forEach(callbackReceivers[notifyItem.name], function (item) { + item.fn(notifyItem); }); }); }); + + var callbackReceivers = {}; + /* Structure of callbackReceivers: + * event_name_one: [ {id:0, fn:fn}, {id:3, fn:fn} ], + * event_name_two: [ {id:2, fn:fn} ], + * */ + var idCounter = 0; + var eventNameRegex = new RegExp('^[a-zA-Z_-]+$'); + var externIdRegex = new RegExp('^[a-zA-Z_-]+\/[0-9]+$'); + return { + registerCallback: function (eventName, fn) { + if (!eventNameRegex.test(eventName)) { + throw 'eventName should only consist of [a-zA-z_-]'; + } else if (typeof fn === 'function') { + var id = idCounter++; + + if (!callbackReceivers[eventName]) { + callbackReceivers[eventName] = []; + } + callbackReceivers[eventName].push({ + id: id, + fn: fn, + }); + return eventName + '/' + id; + } else { + throw 'fn should be a function.'; + } + }, + deregisterCallback: function (externId) { + if (externIdRegex.test(externId)){ + var split = externId.split('/'); + var eventName = split[0]; + var id = parseInt(split[1]); + callbackReceivers[eventName] = _.filter(callbackReceivers[eventName], function (item) { + return item.id !== id; + }); + } else { + throw externId + ' is not a valid id'; + } + }, + // variable length of parameters, just pass ids. + deregisterCallbacks: function () { + _.forEach(arguments, this.deregisterCallback); + }, + notify: function(eventName, params, users, channels) { + if (eventNameRegex.test(eventName)) { + if (!params || typeof params !== 'object') { + params = {}; + } + + var notifyItem = { + collection: 'notify', + name: eventName, + params: params, + users: users, + replyChannels: channels, + }; + if (!operator.user) { + if (!anonymousTrackId) { + anonymousTrackId = Math.floor(Math.random()*1000000); + } + notifyItem.anonymousTrackId = anonymousTrackId; + } + autoupdate.send([notifyItem]); + } else { + throw 'eventName should only consist of [a-zA-z_-]'; + } + }, + }; } ]) @@ -1227,7 +1308,8 @@ angular.module('OpenSlidesApp.core', [ 'Projector', 'ProjectionDefault', 'Tag', - function (ChatMessage, Config, Countdown, ProjectorMessage, Projector, ProjectionDefault, Tag) {} + 'Notify', // For setting up the autoupdate callback + function (ChatMessage, Config, Countdown, ProjectorMessage, Projector, ProjectionDefault, Tag, Notify) {} ]); }()); diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index e50576f96..9d72c7dc9 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -276,7 +276,6 @@ angular.module('OpenSlidesApp.motions.site', [ .factory('MotionForm', [ 'gettextCatalog', 'operator', - 'autoupdate', 'Editor', 'MotionComment', 'Category', @@ -288,18 +287,11 @@ angular.module('OpenSlidesApp.motions.site', [ 'Workflow', 'Agenda', 'AgendaTree', - function (gettextCatalog, operator, autoupdate, Editor, MotionComment, Category, Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree) { + function (gettextCatalog, operator, Editor, MotionComment, Category, Config, Mediafile, MotionBlock, + Tag, User, Workflow, Agenda, AgendaTree) { return { // ngDialog for motion form getDialog: function (motion) { - //TODO: This is just a test implementation. Remove it later. - autoupdate.send([{ - collection: 'notify', - name: 'motion_edit_dialog_opened', - //users: [4, 5, ], - //replyChannels: ["daphne.response.StSjKMGYeq!PfqbSbiJNP", ], - }]); - //End of test implementation return { template: 'static/templates/motions/motion-form.html', controller: motion ? 'MotionUpdateCtrl' : 'MotionCreateCtrl', @@ -1614,6 +1606,8 @@ angular.module('OpenSlidesApp.motions.site', [ .controller('MotionUpdateCtrl', [ '$scope', '$state', + 'operator', + 'gettextCatalog', 'Motion', 'Category', 'Config', @@ -1624,10 +1618,12 @@ angular.module('OpenSlidesApp.motions.site', [ 'Workflow', 'Agenda', 'AgendaUpdate', + 'Notify', + 'Messaging', 'motionId', 'ErrorMessage', - function($scope, $state, Motion, Category, Config, Mediafile, MotionForm, Tag, - User, Workflow, Agenda, AgendaUpdate, motionId, ErrorMessage) { + function($scope, $state, operator, gettextCatalog, Motion, Category, Config, Mediafile, MotionForm, + Tag, User, Workflow, Agenda, AgendaUpdate, Notify, Messaging, motionId, ErrorMessage) { Category.bindAll({}, $scope, 'categories'); Mediafile.bindAll({}, $scope, 'mediafiles'); Tag.bindAll({}, $scope, 'tags'); @@ -1682,7 +1678,97 @@ angular.module('OpenSlidesApp.motions.site', [ } } - // save motion + /* Notify for displaying a warning, if other users edit this motion too */ + var editorNames = []; + var messagingId = 'motionEditDialogOtherUsersWarning'; + var addActiveEditor = function (editorName) { + editorNames.push(editorName); + updateActiveEditors(); + }; + var removeActiveEditor = function (editorName) { + var firstIndex = _.indexOf(editorNames, editorName); + editorNames.splice(firstIndex, 1); + updateActiveEditors(); + }; + var updateActiveEditors = function () { + if (editorNames.length === 0) { + Messaging.deleteMessage(messagingId); + } else { + // This block is only for getting the message string together... + var editorsWithoutAnonymous = _.filter(editorNames, function (name) { + return name; + }); + var text = gettextCatalog.getString('Warning') + ': '; + if (editorsWithoutAnonymous.length === 0) { // Only anonymous + // Singular vs. plural + if (editorNames.length === 1) { + text += gettextCatalog.getString('One anonymous users is also editing this motion.'); + } else { + text += editorNames.length + ' ' + gettextCatalog.getString('anonymous users are also editing this motion.'); + } + } else { + // At least one named user. The max users to display is 5. Anonymous users doesn't get displayed + // by name, but the amount of users is shown. + text += _.slice(editorsWithoutAnonymous, 0, 5).join(', '); + if (editorsWithoutAnonymous.length > 5) { + // More than 5 users with names. + text += ', ... [+' + (editorNames.length - 5) + ']'; + } else if (editorsWithoutAnonymous.length !== editorNames.length) { + // Less than 5 users, so the difference is calculated different. + text += ', ... [+' + (editorNames.length - editorsWithoutAnonymous.length) + ']'; + } + // Singular vs. plural + if (editorNames.length === 1) { + text += ' ' + gettextCatalog.getString('is also editing this motion.'); + } else { + text += ' ' + gettextCatalog.getString('are also editing this motion.'); + } + } + Messaging.createOrEditMessage(messagingId, text, 'warning'); + } + }; + + var responseCallbackId = Notify.registerCallback('motion_dialog_open_response', function (notify) { + if (!notify.sendBySelf && notify.params.motionId === motionId) { + addActiveEditor(notify.params.name); + } + }); + var queryCallbackId = Notify.registerCallback('motion_dialog_open_query', function (notify) { + if (!notify.sendBySelf && notify.params.motionId === motionId) { + addActiveEditor(notify.params.name); + if (notify.senderUserId) { + Notify.notify('motion_dialog_open_response', { + name: operator.user ? operator.user.short_name : '', + motionId: motionId, + }, [notify.senderUserId]); + } else { + Notify.notify('motion_dialog_open_response', { + name: operator.user ? operator.user.short_name : '', + motionId: motionId, + }, null, [notify.senderReplyChannelName]); + } + } + }); + var closeCallbackId = Notify.registerCallback('motion_dialog_closed', function (notify) { + if (notify.params.motionId === motionId) { + removeActiveEditor(notify.params.name); + } + }); + + $scope.$on('$destroy', function () { + Notify.deregisterCallbacks(responseCallbackId, queryCallbackId, closeCallbackId); + Messaging.deleteMessage(messagingId); + Notify.notify('motion_dialog_closed', { + name: operator.user ? operator.user.short_name : '', + motionId: motionId + }); + }); + Notify.notify('motion_dialog_open_query', { + name: operator.user ? operator.user.short_name : '', + motionId: motionId, + }); + + // Save motion $scope.save = function (motion, gotoDetailView) { // inject the changed motion (copy) object back into DS store Motion.inject(motion); diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index 8a0c40c86..419c8daa9 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -115,6 +115,10 @@ def ws_receive_site(message): item['senderReplyChannelName'] = message.reply_channel.name item['senderUserId'] = message.user.id or 0 + # Force the params to be a dict + if not isinstance(item.get('params'), dict): + item['params'] = {} + users = item.get('users') if isinstance(users, list): # Send this item only to all reply channels of some site users.