diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3ed7b194d..e003559c6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,9 @@ https://openslides.org/ Version 2.3 (unreleased) ======================== +Motions: +- New feature to schroll the projector to a specific line [#3748]. + Version 2.2 (2018-06-06) ======================== diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index 06fc85d68..6b620ca6e 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -485,7 +485,7 @@ angular.module('OpenSlidesApp.core', [ deregisterCallbacks: function () { _.forEach(arguments, this.deregisterCallback); }, - notify: function(eventName, params, users, channels) { + notify: function(eventName, params, users, channels, projectors) { if (eventNameRegex.test(eventName)) { if (!params || typeof params !== 'object') { params = {}; @@ -497,6 +497,7 @@ angular.module('OpenSlidesApp.core', [ params: params, users: users, replyChannels: channels, + projectors: projectors, }; if (!operator.user) { if (!anonymousTrackId) { diff --git a/openslides/motions/static/js/motions/projector.js b/openslides/motions/static/js/motions/projector.js index 6a0eca315..d340c78c5 100644 --- a/openslides/motions/static/js/motions/projector.js +++ b/openslides/motions/static/js/motions/projector.js @@ -21,13 +21,40 @@ angular.module('OpenSlidesApp.motions.projector', [ 'Motion', 'MotionChangeRecommendation', 'User', - function($scope, Motion, MotionChangeRecommendation, User) { + 'Notify', + 'ProjectorID', + function($scope, Motion, MotionChangeRecommendation, User, Notify, ProjectorID) { // Attention! Each object that is used here has to be dealt on server side. // Add it to the coresponding get_requirements method of the ProjectorElement // class. var id = $scope.element.id; $scope.mode = $scope.element.mode || 'original'; + var notifyNamePrefix = 'projector_' + ProjectorID() + '_motion_line_'; + var callbackId = Notify.registerCallback(notifyNamePrefix + 'request', function (params) { + var line = params.params.line; + if (!line) { + return; + } + + var scrollTop = null; + $('.line-number-' + line).each(function() { + var top = $(this).offset().top; + if (scrollTop === null || top < scrollTop) { + scrollTop = top; + } + }); + if (scrollTop) { + scrollTop += (-$scope.scroll); // Add the (reversed) scrolling ontop + var scroll = Math.floor((scrollTop/250) - 0.2); + var channel = params.senderReplyChannelName; + Notify.notify(notifyNamePrefix + 'answer', {scroll: scroll}, null, [channel], null); + } + }); + $scope.$on('$destroy', function () { + Notify.deregisterCallback(callbackId); + }); + Motion.bindOne(id, $scope, 'motion'); User.bindAll({}, $scope, 'users'); diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index 9418b8ecd..e04173578 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -1415,6 +1415,7 @@ angular.module('OpenSlidesApp.motions.site', [ 'MotionBlock', 'MotionPdfExport', 'PersonalNoteManager', + 'Notify', 'WebpageTitle', 'EditingWarning', function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog, @@ -1422,7 +1423,7 @@ angular.module('OpenSlidesApp.motions.site', [ MotionStateAndRecommendationParser, MotionChangeRecommendation, Motion, MotionComment, Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing, MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock, - MotionPdfExport, PersonalNoteManager, WebpageTitle, EditingWarning) { + MotionPdfExport, PersonalNoteManager, Notify, WebpageTitle, EditingWarning) { var motion = Motion.get(motionId); Category.bindAll({}, $scope, 'categories'); Mediafile.bindAll({}, $scope, 'mediafiles'); @@ -1538,13 +1539,16 @@ angular.module('OpenSlidesApp.motions.site', [ Motion.bindOne(motion.parent_id, $scope, 'parent'); } + $scope.scrollToLine = 0; $scope.highlight = 0; + $scope.linesForProjector = false; $scope.scrollToAndHighlight = function (line) { + $scope.scrollToLine = line; $scope.highlight = line; // The same line number can occur twice in diff view; we scroll to the first one in this case var scrollTop = null; - $(".line-number-" + line).each(function() { + $('.line-number-' + line).each(function() { var top = $(this).offset().top; if (top > 0 && (scrollTop === null || top < scrollTop)) { scrollTop = top; @@ -1561,6 +1565,29 @@ angular.module('OpenSlidesApp.motions.site', [ $scope.highlight = 0; }, 2000); } + + $scope.scrollProjectorToLine(line); + }; + $scope.scrollProjectorToLine = function (line) { + var projectorIds = $scope.motion.isProjected(); + if (!$scope.linesForProjector || !line || !projectorIds.length) { + return; + } + var projectorId = projectorIds[0]; + var notifyNamePrefix = 'projector_' + projectorId + '_motion_line_'; + + // register callback + var callbackId = Notify.registerCallback(notifyNamePrefix + 'answer', function (params) { + Notify.deregisterCallback(callbackId); + $http.post('/rest/core/projector/' + projectorId + '/set_scroll/', params.params.scroll); + }); + + // Query all projectors + Notify.notify(notifyNamePrefix + 'request', {line: line}, null, null, [projectorId]); + }; + $scope.toggleLinesForProjector = function () { + $scope.linesForProjector = !$scope.linesForProjector; + $scope.scrollProjectorToLine($scope.scrollToLine); }; // open edit dialog diff --git a/openslides/motions/static/templates/motions/motion-detail/toolbar.html b/openslides/motions/static/templates/motions/motion-detail/toolbar.html index d1eefcdb4..df364b238 100644 --- a/openslides/motions/static/templates/motions/motion-detail/toolbar.html +++ b/openslides/motions/static/templates/motions/motion-detail/toolbar.html @@ -79,14 +79,20 @@ e-formclass="small-form" onaftersave="scrollToAndHighlight(gotoLinenumber)"> - +
+ + +
diff --git a/openslides/routing.py b/openslides/routing.py index ff3c7f80e..a40ffd311 100644 --- a/openslides/routing.py +++ b/openslides/routing.py @@ -7,12 +7,14 @@ from openslides.utils.autoupdate import ( ws_add_site, ws_disconnect_projector, ws_disconnect_site, + ws_receive_projector, ws_receive_site, ) projector_routing = [ route("websocket.connect", ws_add_projector), route("websocket.disconnect", ws_disconnect_projector), + route("websocket.receive", ws_receive_projector), ] site_routing = [ diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index 05f909fd5..c8c517826 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -123,9 +123,8 @@ def ws_disconnect_site(message: Any) -> None: @channel_session_user def ws_receive_site(message: Any) -> None: """ - 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. + If we recieve something from the client we currently just interpret this + as a notify message. 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 @@ -138,51 +137,76 @@ def ws_receive_site(message: Any) -> None: pass else: if isinstance(incomming, list): - # Parse all items - receivers_users = defaultdict(list) # type: Dict[int, List[Any]] - receivers_reply_channels = defaultdict(list) # type: Dict[str, List[Any]] - 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 + notify( + incomming, + senderReplyChannelName=message.reply_channel.name, + 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 +def notify(incomming: List[Dict[str, Any]], **attributes: Any) -> None: + """ + The incomming should be a list of notify elements. Every item is broadcasted + to the given users, channels or projectors. If none is given, the message is + send to each site client. + """ + # Parse all items + receivers_users = defaultdict(list) # type: Dict[int, List[Any]] + receivers_projectors = defaultdict(list) # type: Dict[int, List[Any]] + receivers_reply_channels = defaultdict(list) # type: Dict[str, List[Any]] + items_for_all = [] + for item in incomming: + if item.get('collection') == 'notify': + use_receivers_dict = False - 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 + for key, value in attributes.items(): + item[key] = value - if not use_receivers_dict: - # Send this item to all reply channels. - items_for_all.append(item) + # Force the params to be a dict + if not isinstance(item.get('params'), dict): + item['params'] = {} - # 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)}) + 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 - for channel_name, output in receivers_reply_channels.items(): - if len(output) > 0: - send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)}) + projectors = item.get('projectors') + if isinstance(projectors, list): + # Send this item only to all reply channels of some site users. + for projector_id in projectors: + receivers_projectors[projector_id].append(item) + use_receivers_dict = True - if len(items_for_all) > 0: - send_or_wait(Group('site').send, {'text': json.dumps(items_for_all)}) + 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)}) + + for projector_id, output in receivers_projectors.items(): + if len(output) > 0: + send_or_wait(Group('projector-{}'.format(projector_id)).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 @@ -247,6 +271,27 @@ def ws_disconnect_projector(message: Any, projector_id: int) -> None: Group('projector-all').discard(message.reply_channel) +def ws_receive_projector(message: Any, projector_id: int) -> None: + """ + If we recieve something from the client we currently just interpret this + as a notify message. + + The server adds the sender's projector id 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): + notify( + incomming, + senderReplyChannelName=message.reply_channel.name, + senderProjectorId=projector_id) + + def send_data_projector(message: ChannelMessageFormat) -> None: """ Informs all projector clients about changed data.