Merge pull request #2489 from matakuka/issue2211-multiprojector

list-of-speakers overlay (issue #2211, Version 4)
This commit is contained in:
Emanuel Schütze 2016-10-13 09:34:40 +02:00 committed by GitHub
commit ac60fb53da
13 changed files with 216 additions and 30 deletions

View File

@ -21,6 +21,7 @@ Core:
- Added HTML support for messages on the projector. - Added HTML support for messages on the projector.
- Moved custom slides to own app "topics". Renamed it to "Topic". - Moved custom slides to own app "topics". Renamed it to "Topic".
- Added support for multiple projectors. - Added support for multiple projectors.
- Added an overlay for the current list of speakers.
Motions: Motions:
- Added origin field. - Added origin field.

View File

@ -1,5 +1,6 @@
from ..core.config import config from ..core.config import config
from ..core.exceptions import ProjectorException from ..core.exceptions import ProjectorException
from ..core.models import Projector
from ..utils.collection import CollectionElement from ..utils.collection import CollectionElement
from ..utils.projector import ProjectorElement from ..utils.projector import ProjectorElement
from .models import Item from .models import Item
@ -32,7 +33,6 @@ class ItemListSlide(ProjectorElement):
class ListOfSpeakersSlide(ProjectorElement): class ListOfSpeakersSlide(ProjectorElement):
""" """
Slide definitions for Item model. Slide definitions for Item model.
This is only for list of speakers slide. You have to set 'id'. This is only for list of speakers slide. You have to set 'id'.
""" """
name = 'agenda/list-of-speakers' name = 'agenda/list-of-speakers'
@ -69,31 +69,56 @@ class ListOfSpeakersSlide(ProjectorElement):
output.extend(self.get_requirements_as_collection_elements(config_entry)) output.extend(self.get_requirements_as_collection_elements(config_entry))
return output 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. Slide for the current list of speakers.
Nothing special to check.
""" """
name = 'agenda/current-list-of-speakers' name = 'agenda/current-list-of-speakers'
def get_requirements(self, config_entry):
pk = config['projector_currentListOfSpeakers_reference'] class CurrentListOfSpeakersOverlaySlide(CurrentListOfSpeakersMetaClass):
if pk is not None: """
# List of speakers slide. List of speakers overlay.
try: Subclass of ListOfSpeakers
item = Item.objects.get(pk=pk) """
except Item.DoesNotExist: name = 'agenda/current-list-of-speakers-overlay'
# 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

View File

@ -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. // Make sure that the Agenda resource is loaded.
.run(['Agenda', function(Agenda) {}]); .run(['Agenda', function(Agenda) {}]);

View File

@ -16,6 +16,9 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
slidesProvider.registerSlide('agenda/current-list-of-speakers', { slidesProvider.registerSlide('agenda/current-list-of-speakers', {
template: 'static/templates/agenda/slide-current-list-of-speakers.html', 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',
});
} }
]) ])

View File

@ -0,0 +1,19 @@
<div ng-controller="SlideCurrentListOfSpeakersCtrl">
<div ng-if="agendaItem.speakers && agendaItem.speakers.length">
<div id="speakerbox">
<h3 translate>List of speakers</h3>
<p ng-repeat="speaker in lastSpeakers = (agendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))"
class="lastSpeakers">
{{ speaker.user.get_full_name() }}
<p ng-repeat="speaker in agendaItem.speakers | filter: {end_time: null, begin_time: '!!'} "
class="currentSpeaker">
<i class="fa fa-microphone fa-lg"></i> {{ speaker.user.get_full_name() }}
<ol class="nextSpeakers">
<li ng-repeat="speaker in nextSpeakers = (agendaItem.speakers | filter: {begin_time: null}) | orderBy:'weight' | limitTo: 3">
{{ speaker.user.get_full_name() }}
</ol>
<p ng-if="nextSpeakers.length > 3" class="lastSpeakers">
<i>+ {{ nextSpeakers.length - 3 }}</i>
</div>
</div>

View File

@ -54,3 +54,14 @@ class AssignmentSlide(ProjectorElement):
if collection_element == CollectionElement.from_values(Assignment.get_collection_string(), config_entry.get('id')): if collection_element == CollectionElement.from_values(Assignment.get_collection_string(), config_entry.get('id')):
output.extend(self.get_requirements_as_collection_elements(config_entry)) output.extend(self.get_requirements_as_collection_elements(config_entry))
return output 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

View File

@ -30,8 +30,7 @@ class ProjectorManager(models.Manager):
class Projector(RESTModelMixin, models.Model): class Projector(RESTModelMixin, models.Model):
""" """
Model for all projectors. At the moment we support only one projector, Model for all projectors.
the default projector (pk=1).
The config field contains a dictionary which uses UUIDs as keys. Every The config field contains a dictionary which uses UUIDs as keys. Every
element must have at least the property "name". The property "stable" element must have at least the property "name". The property "stable"

View File

@ -196,6 +196,19 @@ h3 {
margin-top: 10px; margin-top: 10px;
margin-bottom: 0px; 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 { ul, ol {
margin: 0 0 10px 2em; margin: 0 0 10px 2em;
} }

View File

@ -714,11 +714,15 @@ angular.module('OpenSlidesApp.core.site', [
'$q', '$q',
'Config', 'Config',
'Projector', '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.countdowns = [];
$scope.highestCountdownIndex = 0; $scope.highestCountdownIndex = 0;
$scope.messages = []; $scope.messages = [];
$scope.highestMessageIndex = 0; $scope.highestMessageIndex = 0;
$scope.listofspeakers = ListOfSpeakersOverlay;
var cancelIntervalTimers = function () { var cancelIntervalTimers = function () {
$scope.countdowns.forEach(function (countdown) { $scope.countdowns.forEach(function (countdown) {
@ -764,18 +768,27 @@ angular.module('OpenSlidesApp.core.site', [
if (!$scope.active_projector) { if (!$scope.active_projector) {
$scope.changeProjector($scope.projectors[0]); $scope.changeProjector($scope.projectors[0]);
} }
$scope.getDefaultOverlayProjector();
// stop ALL interval timer // stop ALL interval timer
cancelIntervalTimers(); cancelIntervalTimers();
rebuildAllElements(); 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() { $scope.$on('$destroy', function() {
// Cancel all intervals if the controller is destroyed // Cancel all intervals if the controller is destroyed
cancelIntervalTimers(); cancelIntervalTimers();
}); });
// watch for changes in projector_broadcast // watch for changes in projector_broadcast and currentListOfSpeakersReference
var last_broadcast; var last_broadcast;
$scope.$watch(function () { $scope.$watch(function () {
return Config.lastModified(); return Config.lastModified();
@ -785,6 +798,7 @@ angular.module('OpenSlidesApp.core.site', [
last_broadcast = broadcast; last_broadcast = broadcast;
$scope.broadcast = broadcast; $scope.broadcast = broadcast;
} }
$scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference');
}); });
$scope.changeProjector = function (projector) { $scope.changeProjector = function (projector) {
@ -1003,6 +1017,13 @@ angular.module('OpenSlidesApp.core.site', [
$scope.preventClose = function (e) { $scope.preventClose = function (e) {
e.stopPropagation(); 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 header and footer');
gettext('Font color of projector headline'); gettext('Font color of projector headline');
gettext('Predefined seconds of new countdowns'); gettext('Predefined seconds of new countdowns');
gettext('List of speakers overlay');
} }
]); ]);

View File

@ -338,6 +338,23 @@
</a> </a>
</div> </div>
</div> </div>
<!-- list of speakers overlay -->
<div class="section" os-perms="core.can_manage_projector">
<a href="#" ng-click="isSpeakerList = !isSpeakerList">
<i class="fa toggle-icon" ng-class="isSpeakerList ? 'fa-angle-up' : 'fa-angle-down'"></i>
<h4 translate>List of speakers</h4>
</a>
<div uib-collapse="!isSpeakerList" ng-cloak>
<projector-button model="listofspeakers" default-projector-id="defaultProjectorId">
</projector-button>
<div class="btn-group" os-perms="agenda.can_manage">
<a ng-click="goToListOfSpeakers()" class="btn btn-default btn-sm"
title="{{ 'Manage current list of speakers' | translate}}">
<i class="fa fa-microphone"></i>
</a>
</div>
</div>
</div>
</div><!-- end div ProjectorControlCtrl --> </div><!-- end div ProjectorControlCtrl -->
</div> </div>

View File

@ -37,3 +37,14 @@ class MotionSlide(ProjectorElement):
if collection_element == CollectionElement.from_values(Motion.get_collection_string(), config_entry.get('id')): if collection_element == CollectionElement.from_values(Motion.get_collection_string(), config_entry.get('id')):
output.extend(self.get_requirements_as_collection_elements(config_entry)) output.extend(self.get_requirements_as_collection_elements(config_entry))
return output 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

View File

@ -22,3 +22,14 @@ class TopicSlide(ProjectorElement):
else: else:
yield topic yield topic
yield topic.agenda_item 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

View File

@ -31,7 +31,8 @@ class ProjectorAPI(TestCase):
'aae4a07b26534cfb9af4232f361dce73': 'aae4a07b26534cfb9af4232f361dce73':
{'id': topic.id, {'id': topic.id,
'uuid': 'aae4a07b26534cfb9af4232f361dce73', 'uuid': 'aae4a07b26534cfb9af4232f361dce73',
'name': 'topics/topic'}}) 'name': 'topics/topic',
'agenda_item_id': topic.agenda_item_id}})
def test_invalid_slide_on_default_projector(self): def test_invalid_slide_on_default_projector(self):
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')