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.