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.
- 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.

View File

@ -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'

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.
.run(['Agenda', function(Agenda) {}]);

View File

@ -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',
});
}
])

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')):
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

View File

@ -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"

View File

@ -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;
}

View File

@ -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');
}
]);

View File

@ -338,6 +338,23 @@
</a>
</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>

View File

@ -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

View File

@ -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

View File

@ -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')