From e6b9b21d4100554b10def586f108158fd7d1addb Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Mon, 12 Sep 2016 11:05:34 +0200 Subject: [PATCH] Added support for multiple projectors. --- CHANGELOG | 1 + openslides/agenda/projector.py | 29 + openslides/agenda/static/js/agenda/base.js | 151 +++-- .../agenda/static/js/agenda/projector.js | 37 +- openslides/agenda/static/js/agenda/site.js | 202 +++++-- .../agenda/current-list-of-speakers.html | 85 +-- .../static/templates/agenda/item-detail.html | 43 +- .../static/templates/agenda/item-list.html | 77 ++- .../slide-current-list-of-speakers.html | 23 + .../assignments/static/js/assignments/base.js | 71 +-- .../assignments/static/js/assignments/site.js | 25 +- .../assignments/assignment-detail.html | 18 +- .../assignments/assignment-list.html | 8 +- openslides/core/apps.py | 5 +- openslides/core/config.py | 14 +- openslides/core/config_variables.py | 29 +- .../core/migrations/0006_multiprojector.py | 54 ++ openslides/core/models.py | 37 +- openslides/core/projector.py | 110 ++-- openslides/core/serializers.py | 15 +- openslides/core/signals.py | 55 ++ openslides/core/static/css/app.css | 110 +++- openslides/core/static/css/projector.css | 19 +- openslides/core/static/js/core/base.js | 184 +++++- openslides/core/static/js/core/projector.js | 128 +++- openslides/core/static/js/core/site.js | 551 +++++++++++++----- .../static/templates/config-form-field.html | 20 - .../templates/core/manage-projectors.html | 200 +++++++ .../templates/core/projector-controls.html | 198 +++++-- .../templates/core/slide_countdown.html | 2 +- .../static/templates/core/slide_message.html | 4 +- .../static/templates/projector-button.html | 27 + .../static/templates/projector-container.html | 8 +- .../core/static/templates/projector.html | 77 +-- openslides/core/urls.py | 4 +- openslides/core/views.py | 82 ++- .../mediafiles/static/js/mediafiles/site.js | 175 +++--- .../templates/mediafiles/mediafile-list.html | 47 +- .../motions/static/js/motions/projector.js | 8 +- openslides/motions/static/js/motions/site.js | 26 +- .../templates/motions/motion-detail.html | 8 +- .../static/templates/motions/motion-list.html | 9 +- openslides/topics/migrations/0001_initial.py | 1 + openslides/topics/static/js/topics/site.js | 12 +- .../static/templates/topics/topic-detail.html | 8 +- openslides/users/static/js/users/site.js | 12 +- .../static/templates/users/user-list.html | 8 +- openslides/utils/autoupdate.py | 16 +- openslides/utils/rest_api.py | 7 +- tests/integration/agenda/test_viewsets.py | 33 +- tests/integration/core/test_views.py | 12 +- 51 files changed, 2272 insertions(+), 813 deletions(-) create mode 100644 openslides/agenda/static/templates/agenda/slide-current-list-of-speakers.html create mode 100644 openslides/core/migrations/0006_multiprojector.py create mode 100644 openslides/core/static/templates/core/manage-projectors.html create mode 100644 openslides/core/static/templates/projector-button.html diff --git a/CHANGELOG b/CHANGELOG index 2245ca1a7..8fe5f6e73 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ Core: - Added support for big assemblies with lots of users. - Added HTML support for messages on the projector. - Moved custom slides to own app "topics". Renamed it to "Topic". +- Added support for multiple projectors Motions: - Added origin field. diff --git a/openslides/agenda/projector.py b/openslides/agenda/projector.py index 07c69b8bb..c93e70124 100644 --- a/openslides/agenda/projector.py +++ b/openslides/agenda/projector.py @@ -64,3 +64,32 @@ class ListOfSpeakersSlide(ProjectorElement): # Full update if item changes because then we may have new speakers # and therefor need new users. return collection_element.collection_string == Item.get_collection_string() + + +class CurrentListOfSpeakersSlide(ProjectorElement): + """ + Slide for the current list of speakers. + + Nothing special to check. + """ + name = 'agenda/current-list-of-speakers' + + def get_requirements(self, config_entry): + pk = config['projector_currentListOfSpeakers_reference'] + if pk is not None: + # List of speakers slide. + try: + item = Item.objects.get(pk=pk) + except Item.DoesNotExist: + # Item does not exist. Just do nothing. + pass + else: + yield item + for speaker in item.speakers.filter(end_time=None): + # Yield current speaker and next speakers + yield speaker.user + query = (item.speakers.exclude(end_time=None) + .order_by('-end_time')[:config['agenda_show_last_speakers']]) + for speaker in query: + # Yield last speakers + yield speaker.user diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js index cf5c62d09..b0302d2ed 100644 --- a/openslides/agenda/static/js/agenda/base.js +++ b/openslides/agenda/static/js/agenda/base.js @@ -112,61 +112,84 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) } }, // override project function of jsDataModel factory - project: function() { - return $http.post( - '/rest/core/projector/1/prune_elements/', - [{name: this.content_object.collection, id: this.content_object.id}] - ); + project: function (projectorId, tree) { + var isProjectedId = this.isProjected(tree); + if (isProjectedId > 0) { + // Deactivate + $http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/'); + } + // Activate, if the projector_id is a new projector. + if (isProjectedId != projectorId) { + var name = tree ? 'agenda/item-list' : this.content_object.collection; + var id = tree ? this.id : this.content_object.id; + return $http.post( + '/rest/core/projector/' + projectorId + '/prune_elements/', + [{name: name, tree: tree, id: id}] + ); + } }, // override isProjected function of jsDataModel factory - isProjected: function (list) { - // Returns true if there is a projector element with the same - // name and the same id. - var projector = Projector.get(1); - var isProjected; - if (typeof projector !== 'undefined') { - var self = this; - var predicate = function (element) { - var value; - if (typeof list === 'undefined') { - // Releated item detail slide - value = element.name == self.content_object.collection && - typeof element.id !== 'undefined' && - element.id == self.content_object.id; - } else { - // Item list slide for sub tree - value = element.name == 'agenda/item-list' && - typeof element.id !== 'undefined' && - element.id == self.id; - } - return value; - }; - isProjected = typeof _.findKey(projector.elements, predicate) === 'string'; - } else { - isProjected = false; + isProjected: function (tree) { + // Returns the id of the last projector with an agenda-item element. Else return 0. + if (typeof tree === 'undefined') { + tree = false; } + var self = this; + var predicate = function (element) { + var value; + if (tree) { + // Item tree slide for sub tree + value = element.name == 'agenda/item-list' && + typeof element.id !== 'undefined' && + element.id == self.id; + } else { + // Releated item detail slide + value = element.name == self.content_object.collection && + typeof element.id !== 'undefined' && + element.id == self.content_object.id; + } + return value; + }; + var isProjected = 0; + Projector.getAll().forEach(function (projector) { + if (typeof _.findKey(projector.elements, predicate) === 'string') { + isProjected = projector.id; + } + }); return isProjected; }, // project list of speakers - projectListOfSpeakers: function() { - return $http.post( - '/rest/core/projector/1/prune_elements/', - [{name: 'agenda/list-of-speakers', id: this.id}] - ); + projectListOfSpeakers: function(projectorId) { + var isProjectedId = this.isListOfSpeakersProjected(); + if (isProjectedId > 0) { + // Deactivate + $http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/'); + } + // Activate + if (isProjectedId != projectorId) { + return $http.post( + '/rest/core/projector/' + projectorId + '/prune_elements/', + [{name: 'agenda/list-of-speakers', id: this.id}] + ); + } }, // check if list of speakers is projected isListOfSpeakersProjected: function () { - // Returns true if there is a projector element with the + // Returns the id of the last projector with an element with the // name 'agenda/list-of-speakers' and the same id. - var projector = Projector.get(1); - if (typeof projector === 'undefined') return false; var self = this; var predicate = function (element) { return element.name == 'agenda/list-of-speakers' && typeof element.id !== 'undefined' && element.id == self.id; }; - return typeof _.findKey(projector.elements, predicate) === 'string'; + var isProjected = 0; + Projector.getAll().forEach(function (projector) { + if (typeof _.findKey(projector.elements, predicate) === 'string') { + isProjected = projector.id; + } + }); + return isProjected; }, hasSubitems: function(items) { var self = this; @@ -265,6 +288,56 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) } ]) +// TODO: Remove all find() calls from the projector logic. It is also used on the site so this has to be +// changed with the refactoring of the site autoupdate. +.factory('CurrentListOfSpeakersItem', [ + 'Projector', + 'Assignment', // TODO: Remove this after refactoring of data loading on start. + 'Topic', // TODO: Remove this after refactoring of data loading on start. + 'Motion', // TODO: Remove this after refactoring of data loading on start. + 'Agenda', + function (Projector, Assignment, Topic, Motion, Agenda) { + return { + getItem: function (projectorId) { + var elementPromise; + return Projector.find(projectorId).then(function (projector) { + // scan all elements + _.forEach(projector.elements, function(element) { + switch(element.name) { + case 'motions/motion': + elementPromise = Motion.find(element.id).then(function(motion) { + return Motion.loadRelations(motion, 'agenda_item').then(function() { + return motion.agenda_item; + }); + }); + break; + case 'topics/topic': + elementPromise = Topic.find(element.id).then(function(topic) { + return Topic.loadRelations(topic, 'agenda_item').then(function() { + return topic.agenda_item; + }); + }); + break; + case 'assignments/assignment': + elementPromise = Assignment.find(element.id).then(function(assignment) { + return Assignment.loadRelations(assignment, 'agenda_item').then(function() { + return assignment.agenda_item; + }); + }); + break; + case 'agenda/list-of-speakers': + elementPromise = Agenda.find(element.id).then(function(item) { + return item; + }); + } + }); + return elementPromise; + }); + } + }; + } +]) + // Make sure that the Agenda resource is loaded. .run(['Agenda', function(Agenda) {}]); diff --git a/openslides/agenda/static/js/agenda/projector.js b/openslides/agenda/static/js/agenda/projector.js index 18f68d12a..074ed90b1 100644 --- a/openslides/agenda/static/js/agenda/projector.js +++ b/openslides/agenda/static/js/agenda/projector.js @@ -13,6 +13,39 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda']) slidesProvider.registerSlide('agenda/item-list', { template: 'static/templates/agenda/slide-item-list.html', }); + slidesProvider.registerSlide('agenda/current-list-of-speakers', { + template: 'static/templates/agenda/slide-current-list-of-speakers.html', + }); + } +]) + +.controller('SlideCurrentListOfSpeakersCtrl', [ + '$scope', + 'Agenda', + 'CurrentListOfSpeakersItem', + 'Config', + function ($scope, Agenda, CurrentListOfSpeakersItem, Config) { + // Watch for changes in the current list of speakers reference + $scope.$watch(function () { + return Config.lastModified('projector_currentListOfSpeakers_reference'); + }, function () { + $scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference'); + $scope.updateCurrentListOfSpeakers(); + }); + // Watch for changes in the current item. + $scope.$watch(function () { + return Agenda.lastModified(); + }, function () { + $scope.updateCurrentListOfSpeakers(); + }); + $scope.updateCurrentListOfSpeakers = function () { + var itemPromise = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference); + if (itemPromise) { + itemPromise.then(function(item) { + $scope.agendaItem = item; + }); + } + }; } ]) @@ -20,7 +53,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda']) '$scope', 'Agenda', 'User', - function($scope, Agenda, User) { + function ($scope, Agenda, User) { // 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. @@ -35,7 +68,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda']) '$filter', 'Agenda', 'AgendaTree', - function($scope, $http, $filter, Agenda, AgendaTree) { + function ($scope, $http, $filter, Agenda, AgendaTree) { // 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. diff --git a/openslides/agenda/static/js/agenda/site.js b/openslides/agenda/static/js/agenda/site.js index 0c458db0f..1b34573a3 100644 --- a/openslides/agenda/static/js/agenda/site.js +++ b/openslides/agenda/static/js/agenda/site.js @@ -99,7 +99,8 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) 'TopicForm', // TODO: Remove this dependency. Use template hook for "New" and "Import" buttons. 'AgendaTree', 'Projector', - function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector) { + 'ProjectionDefault', + function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector, ProjectionDefault) { // Bind agenda tree to the scope $scope.$watch(function () { return Agenda.lastModified(); @@ -110,6 +111,17 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) $scope.agendaHasSubitems = true; } }); + Projector.bindAll({}, $scope, 'projectors'); + $scope.mainListTree = true; + $scope.$watch(function () { + return Projector.lastModified(); + }, function () { + var projectiondefault = ProjectionDefault.filter({name: 'agenda_all_items'})[0]; + if (projectiondefault) { + $scope.defaultProjectorId_all_items = projectiondefault.projector_id; + } + $scope.projectionDefaults = ProjectionDefault.getAll(); + }); $scope.alert = {}; $scope.sumDurations = function () { @@ -218,34 +230,78 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) $scope.uncheckAll(); }; + /** Project functions **/ + // get ProjectionDefault for item + $scope.getProjectionDefault = function (item) { + if (item.tree) { + return $scope.defaultProjectorId_all_items; + } else { + var app_name = item.content_object.collection.split('/')[0]; + var id = 1; + $scope.projectionDefaults.forEach(function (projectionDefault) { + if (projectionDefault.name == app_name) { + id = projectionDefault.projector_id; + } + }); + return id; + } + }; // project agenda - $scope.projectAgenda = function (tree, id) { - $http.post('/rest/core/projector/1/prune_elements/', + $scope.projectAgenda = function (projectorId, tree, id) { + var isAgendaProjectedId = $scope.isAgendaProjected($scope.mainListTree); + if (isAgendaProjectedId > 0) { + // Deactivate + $http.post('/rest/core/projector/' + isAgendaProjectedId + '/prune_elements/', []); + } + if (isAgendaProjectedId != projectorId) { + $http.post('/rest/core/projector/' + projectorId + '/prune_elements/', [{name: 'agenda/item-list', tree: tree, id: id}]); + } + }; + // change whether all items or only main items should be projected + $scope.changeMainListTree = function () { + var isAgendaProjectedId = $scope.isAgendaProjected($scope.mainListTree); + $scope.mainListTree = !$scope.mainListTree; + if (isAgendaProjectedId > 0) { + $scope.projectAgenda(isAgendaProjectedId, $scope.mainListTree); + } + }; + // change whether one item or all subitems should be projected + $scope.changeItemTree = function (item) { + var isProjected = item.isProjected(item.tree); + if (isProjected > 0) { + // Deactivate and reactivate + item.project(isProjected, item.tree); + item.project(isProjected, !item.tree); + } + item.tree = !item.tree; }; // check if agenda is projected $scope.isAgendaProjected = function (tree) { // Returns true if there is a projector element with the name // 'agenda/item-list'. - var projector = Projector.get(1); - if (typeof projector === 'undefined') return false; - var self = this; var predicate = function (element) { var value; - if (typeof tree === 'undefined') { - // only main agenda items - value = element.name == 'agenda/item-list' && - typeof element.id === 'undefined' && - !element.tree; - } else { + if (tree) { // tree with all agenda items value = element.name == 'agenda/item-list' && typeof element.id === 'undefined' && element.tree; + } else { + // only main agenda items + value = element.name == 'agenda/item-list' && + typeof element.id === 'undefined' && + !element.tree; } return value; }; - return typeof _.findKey(projector.elements, predicate) === 'string'; + var projectorId = 0; + $scope.projectors.forEach(function (projector) { + if (typeof _.findKey(projector.elements, predicate) === 'string') { + projectorId = projector.id; + } + }); + return projectorId; }; // auto numbering of agenda items $scope.autoNumbering = function() { @@ -263,9 +319,24 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) 'Agenda', 'User', 'item', - function ($scope, $filter, $http, $state, operator, Agenda, User, item) { + 'Projector', + 'ProjectionDefault', + function ($scope, $filter, $http, $state, operator, Agenda, User, item, Projector, ProjectionDefault) { Agenda.bindOne(item.id, $scope, 'item'); User.bindAll({}, $scope, 'users'); + $scope.$watch(function () { + return Projector.lastModified(); + }, function () { + var item_app_name = item.content_object.collection.split('/')[0]; + var projectiondefaultItem = ProjectionDefault.filter({name: item_app_name})[0]; + if (projectiondefaultItem) { + $scope.defaultProjectorItemId = projectiondefaultItem.projector_id; + } + var projectiondefaultListOfSpeakers = ProjectionDefault.filter({name: 'list_of_speakers'})[0]; + if (projectiondefaultListOfSpeakers) { + $scope.defaultProjectorListOfSpeakersId = projectiondefaultListOfSpeakers.projector_id; + } + }); $scope.speakerSelectBox = {}; $scope.alert = {}; $scope.speakers = $filter('orderBy')(item.speakers, 'weight'); @@ -428,50 +499,69 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda']) '$state', '$http', 'Projector', - 'Assignment', // TODO: Remove this after refactoring of data loading on start. - 'Topic', // TODO: Remove this after refactoring of data loading on start. - 'Motion', // TODO: Remove this after refactoring of data loading on start. - 'Agenda', - function($scope, $state, $http, Projector, Assignment, Topic, Motion, Agenda) { - $scope.$watch( - function() { - return Projector.lastModified(1); - }, - function() { - Projector.find(1).then( function(projector) { - $scope.AgendaItem = null; - _.forEach(projector.elements, function(element) { - switch(element.name) { - case 'motions/motion': - Motion.find(element.id).then(function(motion) { - Motion.loadRelations(motion, 'agenda_item').then(function() { - $scope.AgendaItem = motion.agenda_item; - }); - }); - break; - case 'topics/topic': - Topic.find(element.id).then(function(topic) { - Topic.loadRelations(topic, 'agenda_item').then(function() { - $scope.AgendaItem = topic.agenda_item; - }); - }); - break; - case 'assignments/assignment': - Assignment.find(element.id).then(function(assignment) { - Assignment.loadRelations(assignment, 'agenda_item').then(function() { - $scope.AgendaItem = assignment.agenda_item; - }); - }); - break; - case 'agenda/list-of-speakers': - Agenda.find(element.id).then(function(item) { - $scope.AgendaItem = item; - }); - } - }); + 'ProjectionDefault', + 'Config', + 'CurrentListOfSpeakersItem', + function($scope, $state, $http, Projector, ProjectionDefault, Config, CurrentListOfSpeakersItem) { + // Watch for changes in the current list of speakers reference + $scope.$watch(function () { + return Config.lastModified('projector_currentListOfSpeakers_reference'); + }, function () { + $scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference'); + $scope.updateCurrentListOfSpeakers(); + }); + $scope.$watch(function() { + return Projector.lastModified(); + }, function() { + $scope.projectors = Projector.getAll(); + $scope.updateCurrentListOfSpeakers(); + }); + $scope.$watch(function () { + return Projector.lastModified(); + }, function () { + var projectiondefault = ProjectionDefault.filter({name: 'current_list_of_speakers'})[0]; + if (projectiondefault) { + $scope.defaultProjectorId = projectiondefault.projector_id; + } + }); + + $scope.updateCurrentListOfSpeakers = function () { + var itemPromise = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference); + if (itemPromise) { + itemPromise.then(function(item) { + $scope.AgendaItem = item; }); } - ); + }; + // Project current list of speakers + // same logic as in core/base.js + $scope.projectCurrentLoS = function (projectorId) { + var isCurrentLoSProjectedId = $scope.isCurrentLoSProjected($scope.mainListTree); + if (isCurrentLoSProjectedId > 0) { + // Deactivate + $http.post('/rest/core/projector/' + isCurrentLoSProjectedId + '/prune_elements/', []); + } + if (isCurrentLoSProjectedId != projectorId) { + $http.post('/rest/core/projector/' + projectorId + '/prune_elements/', + [{name: 'agenda/current-list-of-speakers'}]); + } + }; + // same logic as in core/base.js + $scope.isCurrentLoSProjected = function () { + // Returns the projector id if there is a projector element with the name + // 'agenda/current-list-of-speakers'. Elsewise returns 0. + var projectorId = 0; + $scope.projectors.forEach(function (projector) { + var key = _.findKey(projector.elements, function (element) { + return element.name == 'agenda/current-list-of-speakers'; + }); + if (typeof key === 'string') { + projectorId = projector.id; + } + }); + return projectorId; + }; + // go to the list of speakers (management) of the currently // displayed projector slide $scope.goToListOfSpeakers = function() { diff --git a/openslides/agenda/static/templates/agenda/current-list-of-speakers.html b/openslides/agenda/static/templates/agenda/current-list-of-speakers.html index 9b90f7e1d..d9c84943e 100644 --- a/openslides/agenda/static/templates/agenda/current-list-of-speakers.html +++ b/openslides/agenda/static/templates/agenda/current-list-of-speakers.html @@ -1,48 +1,61 @@
-

List of speakers

+

Current list of speakers

{{ AgendaItem.getTitle() }} Closed

+
-
-
-

List of speakers

-

{{ AgendaItem.getTitle() }} - Closed -

-
-
- -

- {{ speaker.user.get_full_name() }} - -

- {{ speaker.user.get_full_name() }} - -

    -
  1. - {{ speaker.user.get_full_name() }} -
-
+
+ +

+ {{ speaker.user.get_full_name() }} + +

+ {{ speaker.user.get_full_name() }} + +

    +
  1. + {{ speaker.user.get_full_name() }} +
diff --git a/openslides/agenda/static/templates/agenda/item-detail.html b/openslides/agenda/static/templates/agenda/item-detail.html index 12a9ac2cf..b9a2377fa 100644 --- a/openslides/agenda/static/templates/agenda/item-detail.html +++ b/openslides/agenda/static/templates/agenda/item-detail.html @@ -10,20 +10,37 @@ {{ item.getContentResource().verboseName | translate }} - - - List of speakers - + + + + + - - - {{ item.getContentResource().verboseName | translate }} - + +

{{ item.getTitle() }}

diff --git a/openslides/agenda/static/templates/agenda/item-list.html b/openslides/agenda/static/templates/agenda/item-list.html index e29581c04..2cd95ef20 100644 --- a/openslides/agenda/static/templates/agenda/item-list.html +++ b/openslides/agenda/static/templates/agenda/item-list.html @@ -38,26 +38,37 @@ Select ... -
- - -
@@ -71,7 +82,7 @@ - List of speakers + Current list of speakers @@ -150,25 +161,37 @@ ng-class="{ 'activeline': item.isProjected(), 'selected': item.selected, 'hiddenrow': item.is_hidden}"> -
- -
diff --git a/openslides/agenda/static/templates/agenda/slide-current-list-of-speakers.html b/openslides/agenda/static/templates/agenda/slide-current-list-of-speakers.html new file mode 100644 index 000000000..b01120afa --- /dev/null +++ b/openslides/agenda/static/templates/agenda/slide-current-list-of-speakers.html @@ -0,0 +1,23 @@ +
+
+

Current List of speakers

+

{{ agendaItem.getTitle() }} + Closed +

+
+ + +

+ {{ speaker.user.get_full_name() }} + +

+ {{ speaker.user.get_full_name() }} + +

    +
  1. + {{ speaker.user.get_full_name() }} +
+
diff --git a/openslides/assignments/static/js/assignments/base.js b/openslides/assignments/static/js/assignments/base.js index e580f0a0b..adfe9342e 100644 --- a/openslides/assignments/static/js/assignments/base.js +++ b/openslides/assignments/static/js/assignments/base.js @@ -267,42 +267,47 @@ angular.module('OpenSlidesApp.assignments', []) return "Election"; }, // override project function of jsDataModel factory - project: function (poll_id) { - return $http.post( - '/rest/core/projector/1/prune_elements/', - [{name: 'assignments/assignment', id: this.id, poll: poll_id}] - ); + project: function (projectorId, pollId) { + var isProjectedId = this.isProjected(pollId); + if (isProjectedId > 0) { + $http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/'); + } + if (isProjectedId != projectorId) { + return $http.post( + '/rest/core/projector/' + projectorId + '/prune_elements/', + [{name: 'assignments/assignment', id: this.id, poll: pollId}] + ); + } }, // override isProjected function of jsDataModel factory isProjected: function (poll_id) { - // Returns true if there is a projector element with the name - // 'assignments/assignment'. - var projector = Projector.get(1); - var isProjected; - if (typeof projector !== 'undefined') { - var self = this; - var predicate = function (element) { - var value; - if (typeof poll_id === 'undefined') { - // Assignment detail slide without poll - value = element.name == 'assignments/assignment' && - typeof element.id !== 'undefined' && - element.id == self.id && - typeof element.poll === 'undefined'; - } else { - // Assignment detail slide with specific poll - value = element.name == 'assignments/assignment' && - typeof element.id !== 'undefined' && - element.id == self.id && - typeof element.poll !== 'undefined' && - element.poll == poll_id; - } - return value; - }; - isProjected = typeof _.findKey(projector.elements, predicate) === 'string'; - } else { - isProjected = false; - } + // Returns the id of the last projector found with an element + // with the name 'assignments/assignment'. + var self = this; + var predicate = function (element) { + var value; + if (typeof poll_id === 'undefined') { + // Assignment detail slide without poll + value = element.name == 'assignments/assignment' && + typeof element.id !== 'undefined' && + element.id == self.id && + typeof element.poll === 'undefined'; + } else { + // Assignment detail slide with specific poll + value = element.name == 'assignments/assignment' && + typeof element.id !== 'undefined' && + element.id == self.id && + typeof element.poll !== 'undefined' && + element.poll == poll_id; + } + return value; + }; + var isProjected = 0; + Projector.getAll().forEach(function (projector) { + if (typeof _.findKey(projector.elements, predicate) === 'string') { + isProjected = projector.id; + } + }); return isProjected; } }, diff --git a/openslides/assignments/static/js/assignments/site.js b/openslides/assignments/static/js/assignments/site.js index c99a0a28f..275221df8 100644 --- a/openslides/assignments/static/js/assignments/site.js +++ b/openslides/assignments/static/js/assignments/site.js @@ -233,9 +233,19 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments']) 'Tag', 'Agenda', 'phases', - function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, phases) { + 'Projector', + 'ProjectionDefault', + function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, phases, Projector, ProjectionDefault) { Assignment.bindAll({}, $scope, 'assignments'); Tag.bindAll({}, $scope, 'tags'); + $scope.$watch(function () { + return Projector.lastModified(); + }, function () { + var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0]; + if (projectiondefault) { + $scope.defaultProjectorId = projectiondefault.projector_id; + } + }); $scope.phases = phases; $scope.alert = {}; @@ -339,10 +349,21 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments']) 'User', 'assignment', 'phases', - function($scope, $http, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment, User, assignment, phases) { + 'Projector', + 'ProjectionDefault', + function($scope, $http, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment, User, + assignment, phases, Projector, ProjectionDefault) { User.bindAll({}, $scope, 'users'); Assignment.bindOne(assignment.id, $scope, 'assignment'); Assignment.loadRelations(assignment, 'agenda_item'); + $scope.$watch(function () { + return Projector.lastModified(); + }, function () { + var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0]; + if (projectiondefault) { + $scope.defaultProjectorId = projectiondefault.projector_id; + } + }); $scope.candidateSelectBox = {}; $scope.phases = phases; $scope.alert = {}; diff --git a/openslides/assignments/static/templates/assignments/assignment-detail.html b/openslides/assignments/static/templates/assignments/assignment-detail.html index 1efbd4577..f02456385 100644 --- a/openslides/assignments/static/templates/assignments/assignment-detail.html +++ b/openslides/assignments/static/templates/assignments/assignment-detail.html @@ -16,12 +16,8 @@ List of speakers - - - + + Published - + + diff --git a/openslides/assignments/static/templates/assignments/assignment-list.html b/openslides/assignments/static/templates/assignments/assignment-list.html index c6cf2bfa2..7cd597e78 100644 --- a/openslides/assignments/static/templates/assignments/assignment-list.html +++ b/openslides/assignments/static/templates/assignments/assignment-list.html @@ -121,12 +121,8 @@ - - - + + diff --git a/openslides/core/apps.py b/openslides/core/apps.py index ede9590c6..81d25365c 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -20,7 +20,7 @@ class CoreAppConfig(AppConfig): from openslides.utils.rest_api import router from openslides.utils.search import index_add_instance, index_del_instance from .config_variables import get_config_variables - from .signals import delete_django_app_permissions + from .signals import delete_django_app_permissions, create_builtin_projection_defaults from .views import ( ChatMessageViewSet, ConfigViewSet, @@ -35,6 +35,9 @@ class CoreAppConfig(AppConfig): post_permission_creation.connect( delete_django_app_permissions, dispatch_uid='delete_django_app_permissions') + post_permission_creation.connect( + create_builtin_projection_defaults, + dispatch_uid='create_builtin_projection_defaults') # Register viewsets. router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) diff --git a/openslides/core/config.py b/openslides/core/config.py index 41df1035f..390f43b5a 100644 --- a/openslides/core/config.py +++ b/openslides/core/config.py @@ -4,16 +4,14 @@ from django.utils.translation import ugettext as _ from .exceptions import ConfigError, ConfigNotFound from .models import ConfigStore -# remove resolution when changing to multiprojector INPUT_TYPE_MAPPING = { 'string': str, 'text': str, 'integer': int, 'boolean': bool, 'choice': str, - 'colorpicker': str, 'comments': list, - 'resolution': dict} + 'colorpicker': str} class ConfigHandler: @@ -89,16 +87,6 @@ class ConfigHandler: except DjangoValidationError as e: raise ConfigError(e.messages[0]) - # remove this block when changing to multiprojector - if config_variable.input_type == 'resolution': - if value.get('width') is None or value.get('height') is None: - raise ConfigError(_('A width and a height have to be given.')) - if not isinstance(value['width'], int) or not isinstance(value['height'], int): - raise ConfigError(_('Data has to be integers.')) - if (value['width'] < 800 or value['width'] > 3840 or - value['height'] < 600 or value['height'] > 2160): - raise ConfigError(_('The Resolution have to be between 800x600 and 3840x2160.')) - if config_variable.input_type == 'comments': if not isinstance(value, list): raise ConfigError(_('motions_comments has to be a list.')) diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py index 7df34013a..52bc715de 100644 --- a/openslides/core/config_variables.py +++ b/openslides/core/config_variables.py @@ -157,11 +157,28 @@ def get_config_variables(): weight=185, group='Projector') - # set the resolution for one projector. It can be removed with the multiprojector feature. yield ConfigVariable( - name='projector_resolution', - default_value={'width': 1024, 'height': 768}, - input_type='resolution', - label='Projector Resolution', - weight=200, + name='projector_blank_color', + default_value='#FFFFFF', + input_type='colorpicker', + label='Color for blanked projector', + weight=190, group='Projector') + + yield ConfigVariable( + name='projector_broadcast', + default_value=0, + input_type='integer', + label='Projector which is broadcasted', + weight=200, + group='Projector', + hidden=True) + + yield ConfigVariable( + name='projector_currentListOfSpeakers_reference', + default_value=1, + input_type='integer', + label='Projector reference for list of speakers', + weight=201, + group='Projector', + hidden=True) diff --git a/openslides/core/migrations/0006_multiprojector.py b/openslides/core/migrations/0006_multiprojector.py new file mode 100644 index 000000000..592695384 --- /dev/null +++ b/openslides/core/migrations/0006_multiprojector.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.9 on 2016-08-29 09:37 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + +import openslides.utils.models + + +def name_default_projector(apps, schema_editor): + """ + Set the name of the default projector to 'Defaultprojector' + """ + Projector = apps.get_model('core', 'Projector') + Projector.objects.filter(pk=1).update(name='Defaultprojector') + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_auto_20160918_2104'), + ] + + operations = [ + migrations.CreateModel( + name='ProjectionDefault', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('display_name', models.CharField(max_length=256)), + ], + options={ + 'default_permissions': (), + }, + bases=(openslides.utils.models.RESTModelMixin, models.Model), + ), + migrations.AddField( + model_name='projector', + name='name', + field=models.CharField(blank=True, max_length=255, unique=True), + ), + migrations.AddField( + model_name='projector', + name='blank', + field=models.BooleanField(blank=False, default=False), + ), + migrations.AddField( + model_name='projectiondefault', + name='projector', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projectiondefaults', to='core.Projector'), + ), + migrations.RunPython(name_default_projector), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index e74272e12..77bba0286 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -63,11 +63,19 @@ class Projector(RESTModelMixin, models.Model): scroll = models.IntegerField(default=0) - # currently unused, but important for the multiprojector. width = models.PositiveIntegerField(default=1024) height = models.PositiveIntegerField(default=768) + name = models.CharField( + max_length=255, + unique=True, + blank=True) + + blank = models.BooleanField( + blank=False, + default=False) + class Meta: """ Contains general permissions that can not be placed in a specific app. @@ -169,6 +177,33 @@ class Projector(RESTModelMixin, models.Model): return result +class ProjectionDefault(RESTModelMixin, models.Model): + """ + Model for the ProjectionDefaults like Motion, Agenda, List of speakers,... + The name is the technical name like 'topics', 'motions'. For apps the name should + be the app name to get keep the ProjectionDefault for apps generic. But it is + possible to give some special name like 'list_of_speakers'. + The display_name is the shown name on the front end for the user. + """ + name = models.CharField(max_length=256) + + display_name = models.CharField(max_length=256) + + projector = models.ForeignKey( + 'Projector', + on_delete=models.CASCADE, + related_name='projectiondefaults') + + def get_root_rest_element(self): + return self.projector + + class Meta: + default_permissions = () + + def __str__(self): + return self.display_name + + class Tag(RESTModelMixin, models.Model): """ Model for tags. This tags can be used for other models like agenda items, diff --git a/openslides/core/projector.py b/openslides/core/projector.py index 52736b235..296d7f2f7 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -1,3 +1,5 @@ +import uuid + from django.utils.timezone import now from ..utils.projector import ProjectorElement @@ -20,7 +22,7 @@ class Countdown(ProjectorElement): To start the countdown write into the config field: { - "status": "running", + "running": True, "countdown_time": , } @@ -30,10 +32,10 @@ class Countdown(ProjectorElement): To stop the countdown set the countdown time to the current value of the countdown (countdown_time = countdown_time - now + serverTimeOffset) - and set status to "stop". + and set running to False. To reset the countdown (it is not a reset in a functional way) just - change the countdown time. The status value remains "stop". + change the countdown time. The running value remains False. Do not forget to send values for additional keywords like "stable" if you do not want to use the default. @@ -63,50 +65,76 @@ class Countdown(ProjectorElement): """ if not isinstance(config_data.get('countdown_time'), (int, float)): raise ProjectorException('Invalid countdown time. Use integer or float.') - if config_data.get('status') not in ('running', 'stop'): - raise ProjectorException("Invalid status. Use 'running' or 'stop'.") + if not isinstance(config_data.get('running'), bool): + raise ProjectorException("Invalid running status. Has to be a boolean.") if config_data.get('default') is not None and not isinstance(config_data.get('default'), int): raise ProjectorException('Invalid default value. Use integer.') @classmethod - def control(cls, action, projector_id=1, index=0): - """ - Starts, stops or resets the countdown with the given index on the - given projector. - - Action must be 'start', 'stop' or 'reset'. - """ + def control(cls, action): if action not in ('start', 'stop', 'reset'): - raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action)) + raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action)) - projector_instance = Projector.objects.get(pk=projector_id) - projector_config = {} - found = False - for key, value in projector_instance.config.items(): - if value['name'] == cls.name: - if index == 0: - try: - cls.validate_config(value) - except ProjectorException: - # Do not proceed if the specific procjector config data is invalid. - # The variable found remains False. - break - found = True - if action == 'start' and value['status'] == 'stop': - value['status'] = 'running' - value['countdown_time'] = now().timestamp() + value['countdown_time'] - elif action == 'stop' and value['status'] == 'running': - value['status'] = 'stop' - value['countdown_time'] = value['countdown_time'] - now().timestamp() - elif action == 'reset': - value['status'] = 'stop' - value['countdown_time'] = value.get('default', config['projector_default_countdown']) - else: - index += -1 - projector_config[key] = value - if found: - projector_instance.config = projector_config - projector_instance.save() + # Use the countdown with the lowest index + projectors = Projector.objects.all() + lowest_index = None + if projectors[0]: + for key, value in projectors[0].config.items(): + if value['name'] == cls.name: + if lowest_index is None or value['index'] < lowest_index: + lowest_index = value['index'] + + if lowest_index is None: + # create a countdown + for projector in projectors: + projector_config = {} + for key, value in projector.config.items(): + projector_config[key] = value + # new countdown + countdown = { + 'name': 'core/countdown', + 'stable': True, + 'index': 1, + 'default_time': config['projector_default_countdown'], + 'visible': False, + 'selected': True, + } + if action == 'start': + countdown['running'] = True + countdown['countdown_time'] = now().timestamp() + countdown['default_time'] + elif action == 'reset' or action == 'stop': + countdown['running'] = False + countdown['countdown_time'] = countdown['default_time'] + projector_config[uuid.uuid4().hex] = countdown + projector.config = projector_config + projector.save() + else: + # search for the countdown and modify it. + for projector in projectors: + projector_config = {} + found = False + for key, value in projector.config.items(): + if value['name'] == cls.name and value['index'] == lowest_index: + try: + cls.validate_config(value) + except ProjectorException: + # Do not proceed if the specific procjector config data is invalid. + # The variable found remains False. + break + found = True + if action == 'start': + value['running'] = True + value['countdown_time'] = now().timestamp() + value['default_time'] + elif action == 'stop' and value['running']: + value['running'] = False + value['countdown_time'] = value['countdown_time'] - now().timestamp() + elif action == 'reset': + value['running'] = False + value['countdown_time'] = value['default_time'] + projector_config[key] = value + if found: + projector.config = projector_config + projector.save() class Message(ProjectorElement): diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index 34ec9c2e2..aeb0482b7 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -1,6 +1,6 @@ from openslides.utils.rest_api import Field, ModelSerializer, ValidationError -from .models import ChatMessage, Projector, Tag +from .models import ChatMessage, ProjectionDefault, Projector, Tag class JSONSerializerField(Field): @@ -22,15 +22,26 @@ class JSONSerializerField(Field): return data +class ProjectionDefaultSerializer(ModelSerializer): + """ + Serializer for core.models.ProjectionDefault objects. + """ + class Meta: + model = ProjectionDefault + fields = ('id', 'name', 'display_name', 'projector', ) + + class ProjectorSerializer(ModelSerializer): """ Serializer for core.models.Projector objects. """ config = JSONSerializerField(write_only=True) + projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True) class Meta: model = Projector - fields = ('id', 'config', 'elements', 'scale', 'scroll', 'width', 'height',) + fields = ('id', 'config', 'elements', 'scale', 'scroll', 'name', 'blank', 'width', 'height', 'projectiondefaults') + read_only_fields = ('scale', 'scroll', 'blank', 'width', 'height', 'projectiondefaults') class TagSerializer(ModelSerializer): diff --git a/openslides/core/signals.py b/openslides/core/signals.py index dc4490f5c..7ec363245 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -3,6 +3,8 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.dispatch import Signal +from .models import ProjectionDefault, Projector + # This signal is sent when the migrate command is done. That means it is sent # after post_migrate sending and creating all Permission objects. Don't use it # for other things than dealing with Permission objects. @@ -20,3 +22,56 @@ def delete_django_app_permissions(sender, **kwargs): Q(app_label='sessions')) for permission in Permission.objects.filter(content_type__in=contenttypes): permission.delete() + + +def create_builtin_projection_defaults(**kwargs): + """ + Creates the builtin defaults: + - agenda_all_items, agenda_item + - assignments + - mediafiles + - motion + - users + - list_of_speakers + - current_list_of_speakers + """ + + # Check whether ProjectionDefaults exists + if ProjectionDefault.objects.all().exists(): + # Do completely nothing if the defaults are already in the database. + return + + default_projector = Projector.objects.get(pk=1) + + ProjectionDefault.objects.create( + name='agenda_all_items', + display_name='Agenda', + projector=default_projector) + ProjectionDefault.objects.create( + name='topics', + display_name='Topics', + projector=default_projector) + ProjectionDefault.objects.create( + name='list_of_speakers', + display_name='List of speakers', + projector=default_projector) + ProjectionDefault.objects.create( + name='current_list_of_speakers', + display_name='Current list of speakers', + projector=default_projector) + ProjectionDefault.objects.create( + name='motions', + display_name='Motions', + projector=default_projector) + ProjectionDefault.objects.create( + name='assignments', + display_name='Elections', + projector=default_projector) + ProjectionDefault.objects.create( + name='users', + display_name='Participants', + projector=default_projector) + ProjectionDefault.objects.create( + name='mediafiles', + display_name='Files', + projector=default_projector) diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index f3ec1263b..c412242c6 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -270,6 +270,12 @@ img { float: right; } +.col1 .header .submenu > div { + display: inline-block; + float: left; + margin-left: 5px; +} + .col1 .meta .title { width: 100%; cursor: pointer; @@ -331,15 +337,6 @@ img { margin-left: 20px; } -/* .resolution can be removed with the multiprojector, but maybe - * it could be reused for the settings in the projectormanage-view */ -.col1 .input-group .resolution { - float: none; - display: inline-block; - width: 80px; - border-radius: 4px !important; -} - /* Toolbar to save motion in inline editing mode */ .motion-save-toolbar { position: fixed; @@ -586,7 +583,6 @@ img { #content .col2 .section a:hover { text-decoration: none; - opacity: 0.5; } #content .toggle-icon { @@ -652,20 +648,20 @@ img { color: #CC0000; } -.col2 .notNull { +.notNull { color: red; font-weight: bold; } /* iframe for live view */ -.col2 #iframe { +.iframe { -moz-transform-origin: 0 0; -webkit-transform-origin: 0 0; -o-transform-origin: 0 0; transform-origin: 0 0 0; } -.col2 #iframewrapper { +.iframewrapper { width: 256px; position: relative; overflow: hidden; @@ -673,7 +669,7 @@ img { margin-bottom: 10px; } -.col2 #iframeoverlay { +.iframeoverlay { width: 256px; position: absolute; top: 0px; @@ -782,6 +778,77 @@ img { margin-bottom: 5px } +/** Pojector sidebar **/ +.col2 .projectorSelector { + margin-top: 10px; + margin-bottom: 10px; +} + +.col2 .projectorSelector > div > div { + width: 65%; + padding-right: 5px; + float: left; +} + +.col2 .projectorSelector > div > div > button { + width: 100%; +} + +.col2 .projectorSelector .manageBtn { + width: 35%; +} + +.col2 .projectorSelector .btn-group { + margin-left: auto; + margin-right: auto; + display: table; +} + +.col2 .projectorSelector > div { + margin-bottom: 10px; +} + +/** manage-projectors **/ + +.projectorContainer > div { + display: inline-table; + width: 256px; + margin: 10px 20px 35px 10px; +} + +.projectorContainer > div > div { + margin-bottom: 10px; +} + +.projectorContainer .middle { + width: 100%; +} + +.projectorContainer .middle > div { + margin-left: auto; + margin-right: auto; + display: table; +} + +.projectorContainer .dropdown { + width: 65%; + padding-right: 5px; + float: left; +} + +.projectorContainer .dropdown > button { + width: 100%; +} + +.projectorContainer .btn-danger { + width: 35%; +} + +.projectorContainer .resolution { + display: inline-block; + width: 120px; +} + /** Footer **/ #footer { float: left; @@ -988,21 +1055,6 @@ img { } /* List of speakers view */ -.fullscreendiv { - position: absolute; - font-size:120%; - top:0; - bottom:0; - right:0; - left:0; - padding: 50px; - background: #fff; - overflow: hidden; - width: 100vw; - height: 100vh; - z-index: 99; -} - .fullscreendiv-title { border-bottom: 5px solid #d3d3d3; margin-bottom: 40px; diff --git a/openslides/core/static/css/projector.css b/openslides/core/static/css/projector.css index e5b358381..778391a30 100644 --- a/openslides/core/static/css/projector.css +++ b/openslides/core/static/css/projector.css @@ -3,7 +3,11 @@ * */ -body{ +html, body { + height: 100%; +} + +body { font-size: 20px !important; line-height: 24px !important; overflow: hidden; @@ -34,7 +38,7 @@ body{ transform-origin: 0 0 0; } -.pContainer #iframewrapper { +.pContainer #iframewrapper, .pContainer .error { position: relative; overflow: hidden; margin-left: auto; @@ -49,6 +53,12 @@ body{ z-index: 1; } +.pContainer .error > p { + color: #f00; + font-size: 150%; + text-align: center; +} + /*** HEADER ***/ #header { box-shadow: 0 0 7px rgba(0,0,0,0.6); @@ -278,6 +288,10 @@ hr { line-height: normal !important; z-index: 301; } +.identify { + background-color: #D9F8C4; + z-index: 400; +} /*** PDF presentation ***/ .rotate0 { @@ -379,7 +393,6 @@ tr.elected td { } - /*** Line numbers ***/ .motion-text .highlight { background-color: #ff0; diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index 44af8e9a7..a9c09c88c 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -33,11 +33,20 @@ angular.module('OpenSlidesApp.core', [ } ]) +.factory('ProjectorID', [ + function () { + return function () { + return /projector\/(\d+)\//.exec(location.pathname)[1]; + }; + } +]) + .factory('autoupdate', [ 'DS', '$rootScope', 'REALM', - function (DS, $rootScope, REALM) { + 'ProjectorID', + function (DS, $rootScope, REALM, ProjectorID) { var socket = null; var recInterval = null; $rootScope.connected = false; @@ -46,8 +55,7 @@ angular.module('OpenSlidesApp.core', [ if (REALM == 'site') { websocketPath = '/ws/site/'; } else if (REALM == 'projector') { - // TODO: At the moment there is only one projector. Find out which one is requested - websocketPath = '/ws/projector/1/'; + websocketPath = '/ws/projector/' + ProjectorID() + '/'; } else { console.error('The constant REALM is not set properly.'); } @@ -246,7 +254,7 @@ angular.module('OpenSlidesApp.core', [ return function () { Config.findAll(); - // Loads all projector data + // Loads all projector data and the projectiondefaults Projector.findAll(); // Loads all chat messages data and their user_ids @@ -305,34 +313,91 @@ angular.module('OpenSlidesApp.core', [ } ]) +// This places a Projectorbutton in the document. Example: +// +// +// This button references to model (in this case 'motion'). Also a defaultProjectionId has to +// be given. In the Exable its a scope variable. The next two parameters are additional: +// - additional-id: Then the model.project and model.isProjected will be called whith this +// argument (ex.: model.project(2)) +// - content: A not trusted text placed behind the projector symbol. +.directive('projectorButton', [ + 'Projector', + function (Projector) { + return { + restrict: 'E', + templateUrl: 'static/templates/projector-button.html', + link: function (scope, element, attributes) { + if (!attributes.model) { + throw 'A model has to be given!'; + } else if (!attributes.defaultProjectorId) { + throw 'A default-projector-id has to be given!'; + } + + Projector.bindAll({}, scope, 'projectors'); + + scope.$watch(attributes.model, function (model) { + scope.model = model; + }); + + scope.$watch(attributes.defaultProjectorId, function (defaultProjectorId) { + scope.defaultProjectorId = defaultProjectorId; + }); + + if (attributes.additionalId) { + scope.$watch(attributes.additionalId, function (id) { + scope.additionalId = id; + }); + } + + if (attributes.content) { + attributes.$observe('content', function (content) { + scope.content = content; + }); + } + } + }; + } +]) + .factory('jsDataModel', [ '$http', 'Projector', function($http, Projector) { var BaseModel = function() {}; - BaseModel.prototype.project = function() { - return $http.post( - '/rest/core/projector/1/prune_elements/', - [{name: this.getResourceName(), id: this.id}] - ); + BaseModel.prototype.project = function(projectorId) { + // if this object is already projected on projectorId, delete this element from this projector + var isProjectedId = this.isProjected(); + if (isProjectedId > 0) { + $http.post('/rest/core/projector/' + isProjectedId + '/prune_elements/'); + } + // if it was the same projector before, just delete it but not show again + if (isProjectedId != projectorId) { + return $http.post( + '/rest/core/projector/' + projectorId + '/prune_elements/', + [{name: this.getResourceName(), id: this.id}] + ); + } }; BaseModel.prototype.isProjected = function() { - // Returns true if there is a projector element with the same - // name and the same id. - var projector = Projector.get(1); - var isProjected; - if (typeof projector !== 'undefined') { - var self = this; - var predicate = function (element) { - return element.name == self.getResourceName() && - typeof element.id !== 'undefined' && - element.id == self.id; - }; - isProjected = typeof _.findKey(projector.elements, predicate) === 'string'; - } else { - isProjected = false; - } - return isProjected; + // Returns the projector id if there is a projector element + // with the same name and the same id. Else returns 0. + // Attention: if this element is projected multiple times, only the + // id of the last projector is returned. + var self = this; + var predicate = function (element) { + return element.name == self.getResourceName() && + typeof element.id !== 'undefined' && + element.id == self.id; + }; + var isProjectedId = 0; + Projector.getAll().forEach(function (projector) { + if (typeof _.findKey(projector.elements, predicate) === 'string') { + isProjectedId = projector.id; + } + }); + return isProjectedId; }; return BaseModel; } @@ -396,10 +461,74 @@ angular.module('OpenSlidesApp.core', [ */ .factory('Projector', [ 'DS', - function(DS) { + '$http', + 'Config', + function(DS, $http, Config) { return DS.defineResource({ name: 'core/projector', onConflict: 'replace', + relations: { + hasMany: { + 'core/projectiondefault': { + localField: 'projectiondefaults', + foreignKey: 'projector_id', + } + }, + }, + methods: { + controlProjector: function(action, direction) { + $http.post('/rest/core/projector/' + this.id + '/control_view/', + {"action": action, "direction": direction} + ); + }, + getStateForCurrentSlide: function () { + var return_dict; + $.each(this.elements, function(key, value) { + if (value.name == 'agenda/list-of-speakers') { + return_dict = { + 'state': 'agenda.item.detail', + 'param': {id: value.id} + }; + } else if ( + value.name != 'agenda/item-list' && + value.name != 'core/clock' && + value.name != 'core/countdown' && + value.name != 'core/message' ) { + return_dict = { + 'state': value.name.replace('/', '.')+'.detail.update', + 'param': {id: value.id} + }; + } + }); + return return_dict; + }, + toggleBlank: function () { + $http.post('/rest/core/projector/' + this.id + '/control_blank/', + !this.blank + ); + }, + toggleBroadcast: function () { + $http.post('/rest/core/projector/' + this.id + '/broadcast/'); + } + }, + }); + } +]) + +/* Model for all projection defaults */ +.factory('ProjectionDefault', [ + 'DS', + function(DS) { + return DS.defineResource({ + name: 'core/projectiondefault', + relations: { + belongsTo: { + 'core/projector': { + localField: 'projector', + localKey: 'projector_id', + } + } + } }); } ]) @@ -522,8 +651,9 @@ angular.module('OpenSlidesApp.core', [ 'ChatMessage', 'Config', 'Projector', + 'ProjectionDefault', 'Tag', - function (ChatMessage, Config, Projector, Tag) {} + function (ChatMessage, Config, Projector, ProjectionDefault, Tag) {} ]); }()); diff --git a/openslides/core/static/js/core/projector.js b/openslides/core/static/js/core/projector.js index 2e7e55163..c0acd41d2 100644 --- a/openslides/core/static/js/core/projector.js +++ b/openslides/core/static/js/core/projector.js @@ -59,25 +59,31 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core']) // Projector Container Controller .controller('ProjectorContainerCtrl', [ '$scope', - 'Config', + '$location', 'loadGlobalData', - function($scope, Config, loadGlobalData) { + 'Projector', + 'ProjectorID', + function($scope, $location, loadGlobalData, Projector, ProjectorID) { loadGlobalData(); - // watch for changes in Config - var last_conf; + + $scope.projector_id = ProjectorID(); + $scope.error = ''; + + // watch for changes in Projector $scope.$watch(function () { - return Config.lastModified(); + return Projector.lastModified($scope.projector_id); }, function () { - // With multiprojector, get the resolution from Prjector.get(pk).{width; height} - if (typeof $scope.config === 'function') { - var conf = $scope.config('projector_resolution'); - if(!last_conf || last_conf.width != conf.width || last_conf.height != conf.height) { - last_conf = conf; - $scope.projectorWidth = conf.width; - $scope.projectorHeight = conf.height; - $scope.recalculateIframe(); + Projector.find($scope.projector_id).then(function (projector) { + $scope.projectorWidth = projector.width; + $scope.projectorHeight = projector.height; + $scope.recalculateIframe(); + }, function (error) { + if (error.status == 404) { + $scope.error = 'Projector not found.'; + } else if (error.status == 403) { + $scope.error = 'You have to login to see the projector.'; } - } + }); }); // recalculate the actual Iframesize and scale @@ -114,27 +120,84 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core']) .controller('ProjectorCtrl', [ '$scope', + '$location', 'Projector', 'slides', - function($scope, Projector, slides) { + 'Config', + 'ProjectorID', + function($scope, $location, Projector, slides, Config, ProjectorID) { + var projector_id = ProjectorID(); + + $scope.broadcast = 0; + + var setElements = function (projector) { + $scope.elements = []; + _.forEach(slides.getElements(projector), function(element) { + if (!element.error) { + $scope.elements.push(element); + } else { + console.error("Error for slide " + element.name + ": " + element.error); + } + }); + }; + $scope.$watch(function () { - // TODO: Use the current projector. At the moment there is only one. - return Projector.lastModified(1); + return Projector.lastModified(projector_id); }, function () { - // TODO: Use the current projector. At the moment there is only one - var projector = Projector.get(1); - if (projector) { + $scope.projector = Projector.get(projector_id); + if ($scope.projector) { + if ($scope.broadcast === 0) { + setElements($scope.projector); + $scope.blank = $scope.projector.blank; + } + } else { + // Blank projector on error $scope.elements = []; - _.forEach(slides.getElements(projector), function(element) { - if (!element.error) { - $scope.elements.push(element); - } else { - console.error("Error for slide " + element.name + ": " + element.error); + $scope.projector = { + scroll: 0, + scale: 0, + blank: true + }; + } + }); + + $scope.$watch(function () { + return Config.lastModified('projector_broadcast'); + }, function () { + Config.findAll().then(function () { + var bc = Config.get('projector_broadcast').value; + if ($scope.broadcast != bc) { + $scope.broadcast = bc; + if ($scope.broadcastDeregister) { + // revert to original $scope.projector + $scope.broadcastDeregister(); + $scope.broadcastDeregister = null; + setElements($scope.projector); + $scope.blank = $scope.projector.blank; } - }); - // TODO: Use the current projector. At the moment there is only one - $scope.scroll = -80 * Projector.get(1).scroll; - $scope.scale = 100 + 20 * Projector.get(1).scale; + } + + if ($scope.broadcast > 0) { + // get elements and blank from broadcast projector + $scope.broadcastDeregister = $scope.$watch(function () { + return Projector.lastModified($scope.broadcast); + }, function () { + if ($scope.broadcast > 0) { + // var broadcast_projector = Projector.get($scope.broadcast); + Projector.find($scope.broadcast).then(function (broadcast_projector) { + setElements(broadcast_projector); + $scope.blank = broadcast_projector.blank; + }); + } + }); + } + }); + }); + + $scope.$on('$destroy', function() { + if ($scope.broadcastDeregister) { + $scope.broadcastDeregister(); + $scope.broadcastDeregister = null; } }); } @@ -158,13 +221,14 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core']) // Add it to the coresponding get_requirements method of the ProjectorElement // class. $scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset ); - $scope.status = $scope.element.status; + $scope.running = $scope.element.running; $scope.visible = $scope.element.visible; + $scope.selected = $scope.element.selected; $scope.index = $scope.element.index; $scope.description = $scope.element.description; // start interval timer if countdown status is running var interval; - if ($scope.status == "running") { + if ($scope.running) { interval = $interval( function() { $scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset ); }, 1000); @@ -186,6 +250,8 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core']) // class. $scope.message = $scope.element.message; $scope.visible = $scope.element.visible; + $scope.selected = $scope.element.selected; + $scope.type = $scope.element.type; } ]); diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index 49b9276a7..825cfdaba 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -717,7 +717,7 @@ angular.module('OpenSlidesApp.core.site', [ templateUrl: 'static/templates/home.html' }) .state('projector', { - url: '/projector', + url: '/projector/{id:int}', templateUrl: 'static/templates/projector-container.html', data: {extern: true}, onEnter: function($window) { @@ -725,13 +725,18 @@ angular.module('OpenSlidesApp.core.site', [ } }) .state('real-projector', { - url: '/real-projector', + url: '/real-projector/{id:int}', templateUrl: 'static/templates/projector.html', data: {extern: true}, onEnter: function($window) { $window.location.href = this.url; } }) + .state('manage-projectors', { + url: '/manage-projectors', + templateUrl: 'static/templates/core/manage-projectors.html', + controller: 'ManageProjectorsCtrl' + }) .state('core', { url: '/core', abstract: true, @@ -901,7 +906,6 @@ angular.module('OpenSlidesApp.core.site', [ 'Config', 'gettextCatalog', function($parse, Config, gettextCatalog) { - // remove resolution when changing to multiprojector function getHtmlType(type) { return { string: 'text', @@ -911,7 +915,6 @@ angular.module('OpenSlidesApp.core.site', [ choice: 'choice', colorpicker: 'colorpicker', comments: 'comments', - resolution: 'resolution', }[type]; } @@ -1187,209 +1190,439 @@ angular.module('OpenSlidesApp.core.site', [ '$http', '$interval', '$state', + '$q', 'Config', 'Projector', - function($scope, $http, $interval, $state, Config, Projector) { - // bind projector elements to the scope, update after projector changed - $scope.$watch(function () { - return Projector.lastModified(1); - }, function () { - // stop ALL interval timer - for (var i=0; i<$scope.countdowns.length; i++) { - if ( $scope.countdowns[i].interval ) { - $interval.cancel($scope.countdowns[i].interval); + function($scope, $http, $interval, $state, $q, Config, Projector) { + $scope.countdowns = []; + $scope.highestCountdownIndex = 0; + $scope.messages = []; + $scope.highestMessageIndex = 0; + + var cancelIntervalTimers = function () { + $scope.countdowns.forEach(function (countdown) { + $interval.cancel(countdown.interval); + }); + }; + + // Get all message and countdown data from the defaultprojector (id=1) + var rebuildAllElements = function () { + $scope.countdowns = []; + $scope.messages = []; + + _.forEach(Projector.get(1).elements, function (element, uuid) { + if (element.name == 'core/countdown') { + $scope.countdowns.push(element); + + if (element.running) { + // calculate remaining seconds directly because interval starts with 1 second delay + $scope.calculateCountdownTime(element); + // start interval timer (every second) + element.interval = $interval(function () { $scope.calculateCountdownTime(element); }, 1000); + } else { + element.seconds = element.countdown_time; + } + + if (element.index > $scope.highestCountdownIndex) { + $scope.highestCountdownIndex = element.index; + } + } else if (element.name == 'core/message') { + $scope.messages.push(element); + + if (element.index > $scope.highestMessageIndex) { + $scope.highestMessageIndex = element.index; + } } + }); + }; + + $scope.$watch(function () { + return Projector.lastModified(); + }, function () { + $scope.projectors = Projector.getAll(); + if (!$scope.active_projector) { + $scope.changeProjector($scope.projectors[0]); } - // rebuild all variables after projector update - $scope.rebuildAllElements(); + + // stop ALL interval timer + cancelIntervalTimers(); + + rebuildAllElements(); }); $scope.$on('$destroy', function() { // Cancel all intervals if the controller is destroyed - for (var i=0; i<$scope.countdowns.length; i++) { - if ( $scope.countdowns[i].interval ) { - $interval.cancel($scope.countdowns[i].interval); - } - } - + cancelIntervalTimers(); }); - // watch for changes in Config - var last_conf; + // watch for changes in projector_broadcast + var last_broadcast; $scope.$watch(function () { return Config.lastModified(); }, function () { - var conf = Config.get('projector_resolution').value; - // With multiprojector, get the resolution from Prjector.get(pk).{width; height} - if(!last_conf || last_conf.width != conf.width || last_conf.height != conf.height) { - last_conf = conf; - $scope.projectorWidth = conf.width; - $scope.projectorHeight = conf.height; - $scope.scale = 256.0 / $scope.projectorWidth; - $scope.iframeHeight = $scope.scale * $scope.projectorHeight; + var broadcast = Config.get('projector_broadcast').value; + if (!last_broadcast || last_broadcast != broadcast) { + last_broadcast = broadcast; + $scope.broadcast = broadcast; } }); + $scope.changeProjector = function (projector) { + $scope.active_projector = projector; + $scope.scale = 256.0 / projector.width; + $scope.iframeHeight = $scope.scale * projector.height; + }; + + $scope.editCurrentSlide = function (projector) { + var state = projector.getStateForCurrentSlide(); + if (state) { + $state.go(state.state, state.param); + } + }; + // *** countdown functions *** $scope.calculateCountdownTime = function (countdown) { countdown.seconds = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset ); }; - $scope.rebuildAllElements = function () { - $scope.countdowns = []; - $scope.messages = []; - // iterate via all projector elements and catch all countdowns and messages - $.each(Projector.get(1).elements, function(key, value) { - if (value.name == 'core/countdown') { - $scope.countdowns.push(value); - if (value.status == "running") { - // calculate remaining seconds directly because interval starts with 1 second delay - $scope.calculateCountdownTime(value); - // start interval timer (every second) - value.interval = $interval( function() { $scope.calculateCountdownTime(value); }, 1000); - } else { - value.seconds = value.countdown_time; + $scope.editCountdown = function (countdown) { + countdown.editFlag = false; + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (element, uuid) { + if (element.name == 'core/countdown' && element.index == countdown.index) { + var data = {}; + data[uuid] = { + "description": countdown.description, + "default_time": parseInt(countdown.default_time) + }; + if (!countdown.running) { + data[uuid].countdown_time = parseInt(countdown.default_time); + } + $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data); } - } - if (value.name == 'core/message') { - $scope.messages.push(value); - } + }); }); - $scope.scrollLevel = Projector.get(1).scroll; - $scope.scaleLevel = Projector.get(1).scale; }; - - // get initial values for $scope.countdowns, $scope.messages, $scope.scrollLevel - // and $scope.scaleLevel (after page reload) - $scope.rebuildAllElements(); - $scope.addCountdown = function () { - var defaultvalue = parseInt(Config.get('projector_default_countdown').value); - $http.post('/rest/core/projector/1/activate_elements/', [{ + var default_time = parseInt($scope.config('projector_default_countdown')); + $scope.highestCountdownIndex++; + // select all projectors on creation, so write the countdown to all projectors + $scope.projectors.forEach(function (projector) { + $http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{ name: 'core/countdown', - status: 'stop', + countdown_time: default_time, + default_time: default_time, visible: false, - index: $scope.countdowns.length, - countdown_time: defaultvalue, - default: defaultvalue, - stable: true - }]); + selected: true, + index: $scope.highestCountdownIndex, + running: false, + stable: true, + }]); + }); }; $scope.removeCountdown = function (countdown) { - var data = {}; - var delta = 0; - // rebuild index for all countdowns after the selected (deleted) countdown - for (var i=0; i<$scope.countdowns.length; i++) { - if ( $scope.countdowns[i].uuid == countdown.uuid ) { - delta = 1; - } else if (delta > 0) { - data[$scope.countdowns[i].uuid] = { "index": i - delta }; - } - } - $http.post('/rest/core/projector/1/deactivate_elements/', [countdown.uuid]); - if (Object.keys(data).length > 0) { - $http.post('/rest/core/projector/1/update_elements/', data); - } - }; - $scope.showCountdown = function (countdown) { - var data = {}; - data[countdown.uuid] = { "visible": !countdown.visible }; - $http.post('/rest/core/projector/1/update_elements/', data); - }; - $scope.editCountdown = function (countdown) { - var data = {}; - data[countdown.uuid] = { - "description": countdown.description, - "default": parseInt(countdown.default) - }; - if (countdown.status == "stop") { - data[countdown.uuid].countdown_time = parseInt(countdown.default); - } - $http.post('/rest/core/projector/1/update_elements/', data); + $scope.projectors.forEach(function (projector) { + var countdowns = []; + _.forEach(projector.elements, function (element, uuid) { + if (element.name == 'core/countdown' && element.index == countdown.index) { + $http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]); + } + }); + }); }; $scope.startCountdown = function (countdown) { - var data = {}; - // calculate end point of countdown (in seconds!) - var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time; - data[countdown.uuid] = { - "status": "running", - "countdown_time": endTimestamp - }; - $http.post('/rest/core/projector/1/update_elements/', data); + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (element, uuid) { + if (element.name == 'core/countdown' && element.index == countdown.index) { + var data = {}; + // calculate end point of countdown (in seconds!) + var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time; + data[uuid] = { + 'running': true, + 'countdown_time': endTimestamp + }; + $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data); + } + }); + }); }; $scope.stopCountdown = function (countdown) { - var data = {}; - // calculate rest duration of countdown (in seconds!) - var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset ); - data[countdown.uuid] = { - "status": "stop", - "countdown_time": newDuration - }; - $http.post('/rest/core/projector/1/update_elements/', data); + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (element, uuid) { + if (element.name == 'core/countdown' && element.index == countdown.index) { + var data = {}; + // calculate rest duration of countdown (in seconds!) + var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset ); + data[uuid] = { + 'running': false, + 'countdown_time': newDuration + }; + $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data); + } + }); + }); }; $scope.resetCountdown = function (countdown) { - var data = {}; - data[countdown.uuid] = { - "status": "stop", - "countdown_time": countdown.default, - }; - $http.post('/rest/core/projector/1/update_elements/', data); + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (element, uuid) { + if (element.name == 'core/countdown' && element.index == countdown.index) { + var data = {}; + data[uuid] = { + 'running': false, + 'countdown_time': countdown.default_time, + }; + $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data); + } + }); + }); }; // *** message functions *** + $scope.editMessage = function (message) { + message.editFlag = false; + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (element, uuid) { + if (element.name == 'core/message' && element.index == message.index) { + var data = {}; + data[uuid] = { + message: message.message, + }; + $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data); + } + }); + }); + }; $scope.addMessage = function () { - $http.post('/rest/core/projector/1/activate_elements/', [{ + $scope.highestMessageIndex++; + // select all projectors on creation, so write the countdown to all projectors + $scope.projectors.forEach(function (projector) { + $http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{ name: 'core/message', visible: false, - index: $scope.messages.length, + selected: true, + index: $scope.highestMessageIndex, message: '', - stable: true - }]); + stable: true, + }]); + }); }; $scope.removeMessage = function (message) { - $http.post('/rest/core/projector/1/deactivate_elements/', [message.uuid]); - }; - $scope.showMessage = function (message) { - var data = {}; - // if current message is activated, deactivate all other messages - if ( !message.visible ) { - for (var i=0; i<$scope.messages.length; i++) { - if ( $scope.messages[i].uuid == message.uuid ) { - data[$scope.messages[i].uuid] = { "visible": true }; - } else { - data[$scope.messages[i].uuid] = { "visible": false }; + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (element, uuid) { + if (element.name == 'core/message' && element.index == message.index) { + $http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]); } - } - } else { - data[message.uuid] = { "visible": false }; - } - $http.post('/rest/core/projector/1/update_elements/', data); - }; - $scope.editMessage = function (message) { - var data = {}; - data[message.uuid] = { - "message": message.message, - }; - $http.post('/rest/core/projector/1/update_elements/', data); - message.editMessageFlag = false; + }); + }); }; - // *** projector controls *** - $scope.scrollLevel = Projector.get(1).scroll; - $scope.scaleLevel = Projector.get(1).scale; - $scope.controlProjector = function (action, direction) { - $http.post('/rest/core/projector/1/control_view/', {"action": action, "direction": direction}); + /* project functions*/ + $scope.project = function (element) { + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (projectorElement, uuid) { + if (element.name == projectorElement.name && element.index == projectorElement.index) { + var data = {}; + data[uuid] = {visible: !projectorElement.visible}; + $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data); + } + }); + }); }; - $scope.editCurrentSlide = function () { - $.each(Projector.get(1).elements, function(key, value) { - if (value.name == 'agenda/list-of-speakers') { - $state.go('agenda.item.detail', {id: value.id}); - } else if ( - value.name != 'agenda/item-list' && - value.name != 'core/clock' && - value.name != 'core/countdown' && - value.name != 'core/message' ) { - $state.go(value.name.replace('/', '.')+'.detail.update', {id: value.id}); + $scope.isProjected = function (element) { + var projectorIds = []; + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (projectorElement, uuid) { + if (element.name == projectorElement.name && element.index == projectorElement.index) { + if (projectorElement.visible && projectorElement.selected) { + projectorIds.push(projector.id); + } + } + }); + }); + return projectorIds; + }; + $scope.isProjectedOn = function (element, projector) { + var projectedIds = $scope.isProjected(element); + return _.indexOf(projectedIds, projector.id) > -1; + }; + $scope.hasProjector = function (element, projector) { + var hasProjector = false; + _.forEach(projector.elements, function (projectorElement, uuid) { + if (element.name == projectorElement.name && element.index == projectorElement.index) { + if (projectorElement.selected) { + hasProjector = true; + } } }); + return hasProjector; + }; + $scope.toggleProjector = function (element, projector) { + _.forEach(projector.elements, function (projectorElement, uuid) { + if (element.name == projectorElement.name && element.index == projectorElement.index) { + var data = {}; + data[uuid] = { + 'selected': !projectorElement.selected, + }; + $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data); + } + }); + }; + $scope.selectAll = function (element, value) { + $scope.projectors.forEach(function (projector) { + _.forEach(projector.elements, function (projectorElement, uuid) { + if (element.name == projectorElement.name && element.index == projectorElement.index) { + var data = {}; + data[uuid] = { + 'selected': value, + }; + $http.post('/rest/core/projector/' + projector.id + '/update_elements/', data); + } + }); + }); + }; + + $scope.preventClose = function (e) { + e.stopPropagation(); + }; + } +]) + +.controller('ManageProjectorsCtrl', [ + '$scope', + '$http', + '$state', + '$timeout', + 'Projector', + 'ProjectionDefault', + 'Config', + 'gettextCatalog', + function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, gettextCatalog) { + ProjectionDefault.bindAll({}, $scope, 'projectiondefaults'); + + // watch for changes in projector_broadcast + // and projector_currentListOfSpeakers_reference + var last_broadcast, last_clos; + $scope.$watch(function () { + return Config.lastModified(); + }, function () { + var broadcast = $scope.config('projector_broadcast'), + currentListOfSpeakers = $scope.config('projector_currentListOfSpeakers_reference'); + if (!last_broadcast || last_broadcast != broadcast) { + last_broadcast = broadcast; + $scope.broadcast = broadcast; + } + if (!last_clos || last_clos != currentListOfSpeakers) { + last_clos = currentListOfSpeakers; + $scope.currentListOfSpeakers = currentListOfSpeakers; + } + }); + + // watch for changes in Projector, and recalc scale and iframeHeight + var first_watch = true; + $scope.resolutions = []; + $scope.edit = []; + $scope.$watch(function () { + return Projector.lastModified(); + }, function () { + $scope.projectors = Projector.getAll(); + $scope.projectors.forEach(function (projector) { + projector.iframeScale = 256.0 / projector.width; + projector.iframeHeight = projector.iframeScale * projector.height; + if (first_watch) { + $scope.resolutions[projector.id] = { + width: projector.width, + height: projector.height + }; + $scope.edit[projector.id] = false; + } + }); + if ($scope.projectors.length) { + first_watch = false; + } + }); + + // Set list of speakers reference + $scope.setListOfSpeakers = function (projector) { + Config.get('projector_currentListOfSpeakers_reference').value = projector.id; + Config.save('projector_currentListOfSpeakers_reference'); + }; + + // Projector functions + $scope.setProjectionDefault = function (projector, def) { + $http.post('/rest/core/projector/' + projector.id + '/set_projectiondefault/', def.id); + }; + $scope.createProjector = function (name) { + var projector = { + name: name, + config: {}, + scale: 0, + scroll: 0, + blank: false, + projectiondefaults: [], + }; + Projector.create(projector).then(function (projector) { + $http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{ + name: 'core/clock', + stable: true + }]); + $scope.resolutions[projector.id] = { + width: projector.width, + height: projector.height + }; + }); + }; + $scope.deleteProjector = function (projector) { + if (projector.id != 1) { + Projector.destroy(projector.id); + } + }; + $scope.editCurrentSlide = function (projector) { + var state = projector.getStateForCurrentSlide(); + if (state) { + $state.go(state.state, state.param); + } + }; + $scope.editName = function (projector) { + projector.config = projector.elements; + Projector.save(projector); + }; + $scope.changeResolution = function (projectorId) { + $http.post( + '/rest/core/projector/' + projectorId + '/set_resolution/', + $scope.resolutions[projectorId] + ).then(function (success) { + $scope.resolutions[projectorId].error = null; + }, function (error) { + $scope.resolutions[projectorId].error = error.data.detail; + }); + }; + + // Identify projectors + $scope.identifyProjectors = function () { + if ($scope.identifyPromise) { + $timeout.cancel($scope.identifyPromise); + $scope.removeIdentifierMessages(); + } else { + $scope.projectors.forEach(function (projector) { + $http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{ + name: 'core/message', + stable: true, + selected: true, + visible: true, + message: gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + projector.name, + type: 'identify' + }]); + }); + $scope.identifyPromise = $timeout($scope.removeIdentifierMessages, 3000); + } + }; + $scope.removeIdentifierMessages = function () { + Projector.getAll().forEach(function (projector) { + $.each(projector.elements, function (uuid, value) { + if (value.name == 'core/message' && value.type == 'identify') { + $http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]); + } + }); + }); + $scope.identifyPromise = null; }; } ]) diff --git a/openslides/core/static/templates/config-form-field.html b/openslides/core/static/templates/config-form-field.html index 177150830..8d9a27a62 100644 --- a/openslides/core/static/templates/config-form-field.html +++ b/openslides/core/static/templates/config-form-field.html @@ -11,26 +11,6 @@ id="{{ key }}" type="{{ type }}"> - - - - - Width: - - Height: - - -
diff --git a/openslides/core/static/templates/core/manage-projectors.html b/openslides/core/static/templates/core/manage-projectors.html new file mode 100644 index 000000000..508611299 --- /dev/null +++ b/openslides/core/static/templates/core/manage-projectors.html @@ -0,0 +1,200 @@ +
+
+ +

Projektoren verwalten

+
+
+ +
+
+ +
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ + x + +
+

+ {{ resolutions[projector.id].error }} +

+
+
+ + + + +
+ +
+
+
+ +
+ + + + + + + + + {{ projector.scale }} + + + + {{ projector.scroll }} +
+ + +
+
+ + +
+
+ +
+
+
diff --git a/openslides/core/static/templates/core/projector-controls.html b/openslides/core/static/templates/core/projector-controls.html index a6d9ed586..5bc4c4585 100644 --- a/openslides/core/static/templates/core/projector-controls.html +++ b/openslides/core/static/templates/core/projector-controls.html @@ -10,10 +10,9 @@
- -
- -
+
+ + +
+ +
- @@ -48,43 +94,43 @@ - {{ scaleLevel }} + {{ active_projector.scale }}
- - -
- {{ scrollLevel }} + {{ active_projector.scroll }}
@@ -96,11 +142,10 @@

Countdowns

-
+
{{ countdown.description }} - Countdown {{ countdown.index +1 }} + Countdown {{ $index +1 }}
+ ng-class="{ 'projected': isProjected(countdown).length }"> - - - +
+ + + +
   - - + - @@ -150,22 +217,29 @@ {{ countdown.seconds | osSecondsToTime }} -
+
- + +
+ +
+
-
-
@@ -190,10 +263,10 @@

Messages

-
+
- {{ 'Message' | translate }} {{ message.index + 1 }} + {{ 'Message' | translate }} {{ $index + 1 }}
+ ng-class="{ 'projected': isProjected(message).length }">
- - - +
+ + + +
+
  
-
+
-
+
-
-
+
+
diff --git a/openslides/core/static/templates/projector-button.html b/openslides/core/static/templates/projector-button.html new file mode 100644 index 000000000..e90dec1c3 --- /dev/null +++ b/openslides/core/static/templates/projector-button.html @@ -0,0 +1,27 @@ +
diff --git a/openslides/core/static/templates/projector-container.html b/openslides/core/static/templates/projector-container.html index 62e904561..d8ed0788c 100644 --- a/openslides/core/static/templates/projector-container.html +++ b/openslides/core/static/templates/projector-container.html @@ -37,10 +37,14 @@ } -
- +
+
+ +
+

{{ error | translate }}

+
diff --git a/openslides/core/static/templates/projector.html b/openslides/core/static/templates/projector.html index f4e59dd7d..16c7ddca3 100644 --- a/openslides/core/static/templates/projector.html +++ b/openslides/core/static/templates/projector.html @@ -11,34 +11,41 @@ - +
+ -