diff --git a/CHANGELOG b/CHANGELOG index 3f14ccdc0..c8661eec5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ Core: - Added HTML support for messages on the projector. - Moved custom slides to own app "topics". Renamed it to "Topic". - Added support for multiple projectors. +- Added an overlay for the current list of speakers. Motions: - Added origin field. diff --git a/openslides/agenda/projector.py b/openslides/agenda/projector.py index e8ce31f5d..db1b10e52 100644 --- a/openslides/agenda/projector.py +++ b/openslides/agenda/projector.py @@ -1,5 +1,6 @@ from ..core.config import config from ..core.exceptions import ProjectorException +from ..core.models import Projector from ..utils.collection import CollectionElement from ..utils.projector import ProjectorElement from .models import Item @@ -32,7 +33,6 @@ class ItemListSlide(ProjectorElement): class ListOfSpeakersSlide(ProjectorElement): """ Slide definitions for Item model. - This is only for list of speakers slide. You have to set 'id'. """ name = 'agenda/list-of-speakers' @@ -69,31 +69,56 @@ class ListOfSpeakersSlide(ProjectorElement): output.extend(self.get_requirements_as_collection_elements(config_entry)) return output + def update_data(self): + return {'agenda_item_id': self.config_entry.get('id')} -class CurrentListOfSpeakersSlide(ProjectorElement): + +class CurrentListOfSpeakersMetaClass(ProjectorElement): + """ + Main class for the list of speaker slides. + + """ + def get_requirements(self, config_entry): + items = self.get_agenda_items(config['projector_currentListOfSpeakers_reference']) + for item in items: + yield item + for speaker in item.speakers.filter(end_time=None): + 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 + + def get_agenda_items(self, projector_id): + projector = Projector.objects.get(pk=projector_id) + for element in projector.elements.values(): + agenda_item_id = element.get('agenda_item_id') + if agenda_item_id is not None: + yield Item.objects.get(pk=agenda_item_id) + + def get_collection_elements_required_for_this(self, collection_element, config_entry): + output = super().get_collection_elements_required_for_this(collection_element, config_entry) + # Full update if agenda_item changes because then we may have new + # candidates and therefor need new users. + items = self.get_agenda_items(config['projector_currentListOfSpeakers_reference']) + for item in items: + if collection_element == CollectionElement.from_values(item.get_collection_string(), item.pk): + output.extend(self.get_requirements_as_collection_elements(config_entry)) + break + return output + + +class CurrentListOfSpeakersSlide(CurrentListOfSpeakersMetaClass): """ 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 + +class CurrentListOfSpeakersOverlaySlide(CurrentListOfSpeakersMetaClass): + """ + List of speakers overlay. + Subclass of ListOfSpeakers + """ + name = 'agenda/current-list-of-speakers-overlay' diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js index b0302d2ed..e4be45c88 100644 --- a/openslides/agenda/static/js/agenda/base.js +++ b/openslides/agenda/static/js/agenda/base.js @@ -298,7 +298,7 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) 'Agenda', function (Projector, Assignment, Topic, Motion, Agenda) { return { - getItem: function (projectorId) { + getItem: function (projectorId) { var elementPromise; return Projector.find(projectorId).then(function (projector) { // scan all elements @@ -338,6 +338,59 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) } ]) +.factory('ListOfSpeakersOverlay', [ + '$http', + 'Projector', + 'gettextCatalog', + 'gettext', + function($http, Projector, gettextCatalog, gettext) { + var name = 'agenda/current-list-of-speakers-overlay'; + return { + name: name, + verboseName: gettext('List of speakers overlay'), + project: function (projectorId, overlay) { + var isProjectedId = this.isProjected(overlay); + if (isProjectedId > 0) { + // Deactivate + var projector = Projector.get(isProjectedId); + var uuid; + _.forEach(projector.elements, function (element) { + if (element.name == 'agenda/current-list-of-speakers-overlay') { + uuid = element.uuid; + } + }); + $http.post('/rest/core/projector/' + isProjectedId + '/deactivate_elements/', + [uuid]); + } + // Activate, if the projector_id is a new projector. + if (isProjectedId != projectorId) { + return $http.post( + '/rest/core/projector/' + projectorId + '/activate_elements/', + [{name: 'agenda/current-list-of-speakers-overlay',stable: true}]); + } + }, + isProjected: function (additionalId) { + // Returns the id of the last projector with an agenda-item element. Else return 0. + // additionalId is not needed + var isProjected = 0; + var predicate = function (element) { + var value; + value = element.name == 'agenda/current-list-of-speakers-overlay'; + return value; + }; + Projector.getAll().forEach(function (projector) { + if (typeof _.findKey(projector.elements, predicate) === 'string') { + isProjected = projector.id; + } + }); + return isProjected; + } + }; + } +]) + + + // 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 074ed90b1..83dccbea7 100644 --- a/openslides/agenda/static/js/agenda/projector.js +++ b/openslides/agenda/static/js/agenda/projector.js @@ -16,6 +16,9 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda']) slidesProvider.registerSlide('agenda/current-list-of-speakers', { template: 'static/templates/agenda/slide-current-list-of-speakers.html', }); + slidesProvider.registerSlide('agenda/current-list-of-speakers-overlay', { + template: 'static/templates/agenda/slide-current-list-of-speakers-overlay.html', + }); } ]) diff --git a/openslides/agenda/static/templates/agenda/slide-current-list-of-speakers-overlay.html b/openslides/agenda/static/templates/agenda/slide-current-list-of-speakers-overlay.html new file mode 100644 index 000000000..02de80af5 --- /dev/null +++ b/openslides/agenda/static/templates/agenda/slide-current-list-of-speakers-overlay.html @@ -0,0 +1,19 @@ +
+
+
+

List of speakers

+

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

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

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

+ + {{ nextSpeakers.length - 3 }} +

+
diff --git a/openslides/assignments/projector.py b/openslides/assignments/projector.py index fac925372..313526d20 100644 --- a/openslides/assignments/projector.py +++ b/openslides/assignments/projector.py @@ -54,3 +54,14 @@ class AssignmentSlide(ProjectorElement): if collection_element == CollectionElement.from_values(Assignment.get_collection_string(), config_entry.get('id')): output.extend(self.get_requirements_as_collection_elements(config_entry)) return output + + def update_data(self): + data = None + try: + assignment = Assignment.objects.get(pk=self.config_entry.get('id')) + except Assignment.DoesNotExist: + # Assignment does not exist, so just do nothing. + pass + else: + data = {'agenda_item_id': assignment.agenda_item_id} + return data diff --git a/openslides/core/models.py b/openslides/core/models.py index d54f0b264..cffa04d76 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -30,8 +30,7 @@ class ProjectorManager(models.Manager): class Projector(RESTModelMixin, models.Model): """ - Model for all projectors. At the moment we support only one projector, - the default projector (pk=1). + Model for all projectors. The config field contains a dictionary which uses UUIDs as keys. Every element must have at least the property "name". The property "stable" diff --git a/openslides/core/static/css/projector.css b/openslides/core/static/css/projector.css index 778391a30..685078bf4 100644 --- a/openslides/core/static/css/projector.css +++ b/openslides/core/static/css/projector.css @@ -196,6 +196,19 @@ h3 { margin-top: 10px; margin-bottom: 0px; } +#speakerbox { + width: 40%; + float: right; + margin: 20px; + right: 0; + bottom: 0; + position: absolute; + background: #d3d3d3; + border-radius: 7px; + border: 1px solid; + padding: 3px 7px 10px 19px; + z-index: 99; +} ul, ol { margin: 0 0 10px 2em; } diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index d31c0f028..ea3c85529 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -714,11 +714,15 @@ angular.module('OpenSlidesApp.core.site', [ '$q', 'Config', 'Projector', - function($scope, $http, $interval, $state, $q, Config, Projector) { + 'CurrentListOfSpeakersItem', + 'ListOfSpeakersOverlay', + 'ProjectionDefault', + function($scope, $http, $interval, $state, $q, Config, Projector, CurrentListOfSpeakersItem, ListOfSpeakersOverlay, ProjectionDefault) { $scope.countdowns = []; $scope.highestCountdownIndex = 0; $scope.messages = []; $scope.highestMessageIndex = 0; + $scope.listofspeakers = ListOfSpeakersOverlay; var cancelIntervalTimers = function () { $scope.countdowns.forEach(function (countdown) { @@ -764,18 +768,27 @@ angular.module('OpenSlidesApp.core.site', [ if (!$scope.active_projector) { $scope.changeProjector($scope.projectors[0]); } - + $scope.getDefaultOverlayProjector(); // stop ALL interval timer cancelIntervalTimers(); rebuildAllElements(); }); + // gets the default projector where the current list of speakers overlay will be displayed + $scope.getDefaultOverlayProjector = function () { + var projectiondefault = ProjectionDefault.filter({name: 'agenda_current_list_of_speakers'})[0]; + if (projectiondefault) { + $scope.defaultProjectorId = projectiondefault.projector_id; + } else { + $scope.defaultProjectorId = 1; + } + }; $scope.$on('$destroy', function() { // Cancel all intervals if the controller is destroyed cancelIntervalTimers(); }); - // watch for changes in projector_broadcast + // watch for changes in projector_broadcast and currentListOfSpeakersReference var last_broadcast; $scope.$watch(function () { return Config.lastModified(); @@ -785,6 +798,7 @@ angular.module('OpenSlidesApp.core.site', [ last_broadcast = broadcast; $scope.broadcast = broadcast; } + $scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference'); }); $scope.changeProjector = function (projector) { @@ -1003,6 +1017,13 @@ angular.module('OpenSlidesApp.core.site', [ $scope.preventClose = function (e) { e.stopPropagation(); }; + + /* go to the list of speakers(management) of the currently displayed list of speakers reference slide*/ + $scope.goToListOfSpeakers = function() { + CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference).then(function (success) { + $state.go('agenda.item.detail', {id: success.id}); + }); + }; } ]) @@ -1406,6 +1427,7 @@ angular.module('OpenSlidesApp.core.site', [ gettext('Font color of projector header and footer'); gettext('Font color of projector headline'); gettext('Predefined seconds of new countdowns'); + gettext('List of speakers overlay'); } ]); diff --git a/openslides/core/static/templates/core/projector-controls.html b/openslides/core/static/templates/core/projector-controls.html index 5bc4c4585..e8d2c4bb3 100644 --- a/openslides/core/static/templates/core/projector-controls.html +++ b/openslides/core/static/templates/core/projector-controls.html @@ -338,6 +338,23 @@
+ +
+ + +

List of speakers

+
+
+ + +
+ + + +
+
+
diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index 07e006bfa..862961662 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -37,3 +37,14 @@ class MotionSlide(ProjectorElement): if collection_element == CollectionElement.from_values(Motion.get_collection_string(), config_entry.get('id')): output.extend(self.get_requirements_as_collection_elements(config_entry)) return output + + def update_data(self): + data = None + try: + motion = Motion.objects.get(pk=self.config_entry.get('id')) + except Motion.DoesNotExist: + # Motion does not exist, so just do nothing. + pass + else: + data = {'agenda_item_id': motion.agenda_item_id} + return data diff --git a/openslides/topics/projector.py b/openslides/topics/projector.py index 642379452..929b15ce1 100644 --- a/openslides/topics/projector.py +++ b/openslides/topics/projector.py @@ -22,3 +22,14 @@ class TopicSlide(ProjectorElement): else: yield topic yield topic.agenda_item + + def update_data(self): + data = None + try: + topic = Topic.objects.get(pk=self.config_entry.get('id')) + except Topic.DoesNotExist: + # Topic does not exist, so just do nothing. + pass + else: + data = {'agenda_item_id': topic.agenda_item_id} + return data diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py index d0255d417..23bc01d51 100644 --- a/tests/integration/core/test_views.py +++ b/tests/integration/core/test_views.py @@ -31,7 +31,8 @@ class ProjectorAPI(TestCase): 'aae4a07b26534cfb9af4232f361dce73': {'id': topic.id, 'uuid': 'aae4a07b26534cfb9af4232f361dce73', - 'name': 'topics/topic'}}) + 'name': 'topics/topic', + 'agenda_item_id': topic.agenda_item_id}}) def test_invalid_slide_on_default_projector(self): self.client.login(username='admin', password='admin')