diff --git a/CHANGELOG b/CHANGELOG
index 2245ca1a7..17146e2e6 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..7ca0ee866 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 + '/clear_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: 'agenda_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,64 @@ 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();
+ var projectiondefault = ProjectionDefault.filter({name: 'agenda_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 + '/clear_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 @@
-
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 ...
-
-
+
+ ng-click="projectAgenda(defaultProjectorId_all_items, mainListTree)"
+ ng-class="{ 'btn-primary': isAgendaProjected(mainListTree) > 0 && isAgendaProjected(mainListTree) == defaultProjectorId_all_items}">
Agenda
-
+
-
@@ -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}">
-
-
+
+ ng-click="item.project(getProjectionDefault(item), item.tree)"
+ ng-class="{ 'btn-primary': item.isProjected(item.tree) > 0 && item.isProjected(item.tree) == getProjectionDefault(item)}">
-
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() }}
+
+
+
+ {{ 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
-
-
- 4. Project
-
+
+
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..638ceb839
--- /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='Default projector')
+
+
+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..953496b42 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,34 @@ class Projector(RESTModelMixin, models.Model):
return result
+class ProjectionDefault(RESTModelMixin, models.Model):
+ """
+ Model for the projection defaults like motions, agenda, list of
+ speakers and thelike. The name is the technical name like 'topics' or
+ '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..a3c45dd89 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', )
class TagSerializer(ModelSerializer):
diff --git a/openslides/core/signals.py b/openslides/core/signals.py
index dc4490f5c..92609c323 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,58 @@ 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_list_of_speakers, agenda_current_list_of_speakers
+ - topics
+ - assignments
+ - mediafiles
+ - motion
+ - users
+
+ These strings have to be used in the controllers where you want to
+ define a projector button. Use the string to get the id of the
+ responsible projector and pass this id to the projector button directive.
+ """
+ # Check whether ProjectionDefault objects exist.
+ if ProjectionDefault.objects.all().exists():
+ # Do completely nothing if some 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='agenda_list_of_speakers',
+ display_name='List of speakers',
+ projector=default_projector)
+ ProjectionDefault.objects.create(
+ name='agenda_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..0a66870a1 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.');
}
@@ -184,10 +192,12 @@ angular.module('OpenSlidesApp.core', [
autoupdate.onMessage(function(json) {
// TODO: when MODEL.find() is called after this
// a new request is fired. This could be a bug in DS
- // TODO: If you don't have the permission to see a projector, the
- // variable json is a string with an error message. Therefor
- // the next line fails.
- var dataList = JSON.parse(json);
+ var dataList = [];
+ try {
+ dataList = JSON.parse(json);
+ } catch(err) {
+ console.error(json);
+ }
_.forEach(dataList, function(data) {
console.log("Received object: " + data.collection + ", " + data.id);
var instance = DS.get(data.collection, data.id);
@@ -246,7 +256,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 +315,93 @@ angular.module('OpenSlidesApp.core', [
}
])
+/*
+ * This places a projector button in the document.
+ *
+ * Example:
+ * This button references to model (in this example 'motion'). Also a defaultProjectionId
+ * has to be given. In the example it's a scope variable. The next two parameters are additional:
+ * - additional-id: Then the model.project and model.isProjected will be called with
+ * this argument (e. g.: model.project(2))
+ * - content: A 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 + '/clear_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 +465,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;
+ angular.forEach(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',
+ }
+ }
+ }
});
}
])
@@ -463,11 +596,13 @@ angular.module('OpenSlidesApp.core', [
}
])
-// This filter filters all items in array. If the filterArray is empty, the array is passed.
-// The filterArray contains numbers of the multiselect: [1, 3, 4].
-// Then, all items in array are passed, if the item_id (get with id_function) matches one of the
-// ids in filterArray. id_function could also return a list of ids. Example:
-// Item 1 has two tags with ids [1, 4]. filterArray = [3, 4] --> match
+/*
+ * This filter filters all items in an array. If the filterArray is empty, the
+ * array is passed. The filterArray contains numbers of the multiselect, e. g. [1, 3, 4].
+ * Then, all items in the array are passed, if the item_id (get with id_function) matches
+ * one of the ids in filterArray. id_function could also return a list of ids. Example:
+ * Item 1 has two tags with ids [1, 4]. filterArray == [3, 4] --> match
+ */
.filter('SelectMultipleFilter', [
function () {
return function (array, filterArray, idFunction) {
@@ -522,8 +657,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..00b423e1c 100644
--- a/openslides/core/static/js/core/projector.js
+++ b/openslides/core/static/js/core/projector.js
@@ -59,24 +59,29 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Projector Container Controller
.controller('ProjectorContainerCtrl', [
'$scope',
- 'Config',
+ '$location',
+ 'gettext',
'loadGlobalData',
- function($scope, Config, loadGlobalData) {
+ 'Projector',
+ 'ProjectorID',
+ function($scope, $location, gettext, 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();
- }
+ var projector = Projector.get($scope.projector_id);
+ if (projector) {
+ $scope.error = '';
+ $scope.projectorWidth = projector.width;
+ $scope.projectorHeight = projector.height;
+ $scope.recalculateIframe();
+ } else {
+ $scope.error = gettext('Can not open the projector.');
}
});
@@ -114,27 +119,83 @@ 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 () {
+ var bc = Config.get('projector_broadcast');
+ if (bc) {
+ if ($scope.broadcast != bc.value) {
+ $scope.broadcast = bc.value;
+ 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);
+ if (broadcast_projector) {
+ setElements(broadcast_projector);
+ $scope.blank = broadcast_projector.blank;
+ }
+ }
+ });
+ }
+ }
+ });
+
+ $scope.$on('$destroy', function() {
+ if ($scope.broadcastDeregister) {
+ $scope.broadcastDeregister();
+ $scope.broadcastDeregister = null;
}
});
}
@@ -158,13 +219,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 +248,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..beb1694cf 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) {
+ angular.forEach(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 :
-
-
-
+-
+
+ {{ projector.name }}
+
+
+Projektoren verwalten
++-
+
+ {{ projectiondefault.display_name | translate }}
+
+
++ {{ resolutions[projector.id].error }} +
++-
+
+ {{ projector.name }}
+
+
+
+Countdowns
+-
+
+ All
+
+ |
+ None
+
+
+
+ -
+
+
+
+ {{ projector.name }}
+
+
+
+Messages