Merge pull request #3212 from normanjaeckel/Notify
Added notify system.
This commit is contained in:
commit
9387f83f8a
@ -23,6 +23,8 @@ Motions:
|
|||||||
- Added support for export motions in a zip archive [#3189].
|
- Added support for export motions in a zip archive [#3189].
|
||||||
- Bugfix: changing motion line length did not invalidate cache [#3202]
|
- Bugfix: changing motion line length did not invalidate cache [#3202]
|
||||||
- Bugfix: Added more distance in motion PDF for DEL-tags in new lines [#3211].
|
- Bugfix: Added more distance in motion PDF for DEL-tags in new lines [#3211].
|
||||||
|
- Added warning message if an edit dialog was already opened by another
|
||||||
|
client [#3212].
|
||||||
|
|
||||||
Users:
|
Users:
|
||||||
- User without permission to see users can now see agenda item speakers,
|
- User without permission to see users can now see agenda item speakers,
|
||||||
@ -38,6 +40,7 @@ Core:
|
|||||||
application [#3172].
|
application [#3172].
|
||||||
- Adding support for choosing image files as logos [#3184, #3207].
|
- Adding support for choosing image files as logos [#3184, #3207].
|
||||||
- Fixing error when clearing empty chat [#3199].
|
- Fixing error when clearing empty chat [#3199].
|
||||||
|
- Added notify system [#3212].
|
||||||
|
|
||||||
General:
|
General:
|
||||||
- Several bugfixes and minor improvements.
|
- Several bugfixes and minor improvements.
|
||||||
|
@ -1196,7 +1196,7 @@ img {
|
|||||||
border-color: #d6e9c6;
|
border-color: #d6e9c6;
|
||||||
}
|
}
|
||||||
#messaging .warning {
|
#messaging .warning {
|
||||||
color: #8a6d3b;
|
color: #725523;
|
||||||
background-color: #fcf8e3;
|
background-color: #fcf8e3;
|
||||||
border-color: #faebcc;
|
border-color: #faebcc;
|
||||||
}
|
}
|
||||||
|
@ -89,9 +89,15 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
socket.onmessage = function (event) {
|
socket.onmessage = function (event) {
|
||||||
|
var dataList = [];
|
||||||
|
try {
|
||||||
|
dataList = JSON.parse(event.data);
|
||||||
_.forEach(Autoupdate.messageReceivers, function (receiver) {
|
_.forEach(Autoupdate.messageReceivers, function (receiver) {
|
||||||
receiver(event.data);
|
receiver(dataList);
|
||||||
});
|
});
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
// Check if the promise is not resolved yet.
|
// Check if the promise is not resolved yet.
|
||||||
if (Autoupdate.firstMessageDeferred.promise.$$state.status === 0) {
|
if (Autoupdate.firstMessageDeferred.promise.$$state.status === 0) {
|
||||||
Autoupdate.firstMessageDeferred.resolve();
|
Autoupdate.firstMessageDeferred.resolve();
|
||||||
@ -99,6 +105,11 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
ErrorMessage.clearConnectionError();
|
ErrorMessage.clearConnectionError();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Autoupdate.send = function (message) {
|
||||||
|
if (socket) {
|
||||||
|
socket.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
};
|
||||||
Autoupdate.closeConnection = function () {
|
Autoupdate.closeConnection = function () {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.close();
|
socket.close();
|
||||||
@ -262,16 +273,8 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
'autoupdate',
|
'autoupdate',
|
||||||
'dsEject',
|
'dsEject',
|
||||||
function (DS, autoupdate, dsEject) {
|
function (DS, autoupdate, dsEject) {
|
||||||
autoupdate.onMessage(function(json) {
|
// Handler for normal autoupdate messages.
|
||||||
// TODO: when MODEL.find() is called after this
|
autoupdate.onMessage(function(dataList) {
|
||||||
// a new request is fired. This could be a bug in DS
|
|
||||||
var dataList = [];
|
|
||||||
try {
|
|
||||||
dataList = JSON.parse(json);
|
|
||||||
} catch(err) {
|
|
||||||
console.error(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
var dataListByCollection = _.groupBy(dataList, 'collection');
|
var dataListByCollection = _.groupBy(dataList, 'collection');
|
||||||
_.forEach(dataListByCollection, function (list, key) {
|
_.forEach(dataListByCollection, function (list, key) {
|
||||||
var changedElements = [];
|
var changedElements = [];
|
||||||
@ -281,6 +284,8 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
// Uncomment this line for debugging to log all autoupdates:
|
// Uncomment this line for debugging to log all autoupdates:
|
||||||
// console.log("Received object: " + data.collection + ", " + data.id);
|
// console.log("Received object: " + data.collection + ", " + data.id);
|
||||||
|
|
||||||
|
// Now handle autoupdate message but do not handle notify messages.
|
||||||
|
if (data.collection !== 'notify') {
|
||||||
// remove (=eject) object from local DS store
|
// remove (=eject) object from local DS store
|
||||||
var instance = DS.get(data.collection, data.id);
|
var instance = DS.get(data.collection, data.id);
|
||||||
if (instance) {
|
if (instance) {
|
||||||
@ -295,6 +300,7 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
console.error('Error: Undefined action for received object' +
|
console.error('Error: Undefined action for received object' +
|
||||||
'(' + data.collection + ', ' + data.id + ')');
|
'(' + data.collection + ', ' + data.id + ')');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// add (=inject) all given objects into local DS store
|
// add (=inject) all given objects into local DS store
|
||||||
if (changedElements.length > 0) {
|
if (changedElements.length > 0) {
|
||||||
@ -310,6 +316,108 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
.factory('Notify', [
|
||||||
|
'autoupdate',
|
||||||
|
'operator',
|
||||||
|
function (autoupdate, operator) {
|
||||||
|
var anonymousTrackId;
|
||||||
|
|
||||||
|
// Handler for notify messages.
|
||||||
|
autoupdate.onMessage(function(dataList) {
|
||||||
|
var dataListByCollection = _.groupBy(dataList, 'collection');
|
||||||
|
_.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_-]';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// Save the server time to the rootscope.
|
// Save the server time to the rootscope.
|
||||||
.run([
|
.run([
|
||||||
'$http',
|
'$http',
|
||||||
@ -1200,7 +1308,8 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
'Projector',
|
'Projector',
|
||||||
'ProjectionDefault',
|
'ProjectionDefault',
|
||||||
'Tag',
|
'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) {}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
}());
|
}());
|
||||||
|
@ -287,7 +287,8 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'Workflow',
|
'Workflow',
|
||||||
'Agenda',
|
'Agenda',
|
||||||
'AgendaTree',
|
'AgendaTree',
|
||||||
function (gettextCatalog, operator, 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 {
|
return {
|
||||||
// ngDialog for motion form
|
// ngDialog for motion form
|
||||||
getDialog: function (motion) {
|
getDialog: function (motion) {
|
||||||
@ -1682,6 +1683,8 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
.controller('MotionUpdateCtrl', [
|
.controller('MotionUpdateCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'$state',
|
'$state',
|
||||||
|
'operator',
|
||||||
|
'gettextCatalog',
|
||||||
'Motion',
|
'Motion',
|
||||||
'Category',
|
'Category',
|
||||||
'Config',
|
'Config',
|
||||||
@ -1692,10 +1695,12 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'Workflow',
|
'Workflow',
|
||||||
'Agenda',
|
'Agenda',
|
||||||
'AgendaUpdate',
|
'AgendaUpdate',
|
||||||
|
'Notify',
|
||||||
|
'Messaging',
|
||||||
'motionId',
|
'motionId',
|
||||||
'ErrorMessage',
|
'ErrorMessage',
|
||||||
function($scope, $state, Motion, Category, Config, Mediafile, MotionForm, Tag,
|
function($scope, $state, operator, gettextCatalog, Motion, Category, Config, Mediafile, MotionForm,
|
||||||
User, Workflow, Agenda, AgendaUpdate, motionId, ErrorMessage) {
|
Tag, User, Workflow, Agenda, AgendaUpdate, Notify, Messaging, motionId, ErrorMessage) {
|
||||||
Category.bindAll({}, $scope, 'categories');
|
Category.bindAll({}, $scope, 'categories');
|
||||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||||
Tag.bindAll({}, $scope, 'tags');
|
Tag.bindAll({}, $scope, 'tags');
|
||||||
@ -1750,7 +1755,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) {
|
$scope.save = function (motion, gotoDetailView) {
|
||||||
// inject the changed motion (copy) object back into DS store
|
// inject the changed motion (copy) object back into DS store
|
||||||
Motion.inject(motion);
|
Motion.inject(motion);
|
||||||
|
@ -6,6 +6,7 @@ from openslides.utils.autoupdate import (
|
|||||||
ws_add_site,
|
ws_add_site,
|
||||||
ws_disconnect_projector,
|
ws_disconnect_projector,
|
||||||
ws_disconnect_site,
|
ws_disconnect_site,
|
||||||
|
ws_receive_site,
|
||||||
)
|
)
|
||||||
|
|
||||||
projector_routing = [
|
projector_routing = [
|
||||||
@ -16,6 +17,7 @@ projector_routing = [
|
|||||||
site_routing = [
|
site_routing = [
|
||||||
route("websocket.connect", ws_add_site),
|
route("websocket.connect", ws_add_site),
|
||||||
route("websocket.disconnect", ws_disconnect_site),
|
route("websocket.disconnect", ws_disconnect_site),
|
||||||
|
route("websocket.receive", ws_receive_site),
|
||||||
]
|
]
|
||||||
|
|
||||||
channel_routing = [
|
channel_routing = [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
from collections import Iterable
|
from collections import Iterable, defaultdict
|
||||||
|
|
||||||
from channels import Channel, Group
|
from channels import Channel, Group
|
||||||
from channels.asgi import get_channel_layer
|
from channels.asgi import get_channel_layer
|
||||||
@ -88,6 +88,71 @@ def ws_disconnect_site(message):
|
|||||||
websocket_user_cache.remove(message.user.id or 0, message.reply_channel.name)
|
websocket_user_cache.remove(message.user.id or 0, message.reply_channel.name)
|
||||||
|
|
||||||
|
|
||||||
|
@channel_session_user
|
||||||
|
def ws_receive_site(message):
|
||||||
|
"""
|
||||||
|
This function is called if a message from a client comes in. The message
|
||||||
|
should be a list. Every item is broadcasted to the given users (or all
|
||||||
|
users if no user list is given) if it is a notify element.
|
||||||
|
|
||||||
|
The server adds the sender's user id (0 for anonymous) and reply
|
||||||
|
channel name so that a receiver client may reply to the sender or to all
|
||||||
|
sender's instances.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
incomming = json.loads(message.content['text'])
|
||||||
|
except ValueError:
|
||||||
|
# Message content is invalid. Just do nothing.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(incomming, list):
|
||||||
|
# Parse all items
|
||||||
|
receivers_users = defaultdict(list)
|
||||||
|
receivers_reply_channels = defaultdict(list)
|
||||||
|
items_for_all = []
|
||||||
|
for item in incomming:
|
||||||
|
if item.get('collection') == 'notify':
|
||||||
|
use_receivers_dict = False
|
||||||
|
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.
|
||||||
|
for user_id in users:
|
||||||
|
receivers_users[user_id].append(item)
|
||||||
|
use_receivers_dict = True
|
||||||
|
|
||||||
|
reply_channels = item.get('replyChannels')
|
||||||
|
if isinstance(reply_channels, list):
|
||||||
|
# Send this item only to some reply channels.
|
||||||
|
for reply_channel_name in reply_channels:
|
||||||
|
receivers_reply_channels[reply_channel_name].append(item)
|
||||||
|
use_receivers_dict = True
|
||||||
|
|
||||||
|
if not use_receivers_dict:
|
||||||
|
# Send this item to all reply channels.
|
||||||
|
items_for_all.append(item)
|
||||||
|
|
||||||
|
# Send all items
|
||||||
|
for user_id, channel_names in websocket_user_cache.get_all().items():
|
||||||
|
output = receivers_users[user_id]
|
||||||
|
if len(output) > 0:
|
||||||
|
for channel_name in channel_names:
|
||||||
|
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
|
||||||
|
|
||||||
|
for channel_name, output in receivers_reply_channels.items():
|
||||||
|
if len(output) > 0:
|
||||||
|
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
|
||||||
|
|
||||||
|
if len(items_for_all) > 0:
|
||||||
|
send_or_wait(Group('site').send, {'text': json.dumps(items_for_all)})
|
||||||
|
|
||||||
|
|
||||||
@channel_session_user_from_http
|
@channel_session_user_from_http
|
||||||
def ws_add_projector(message, projector_id):
|
def ws_add_projector(message, projector_id):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user