list-of-speakers overlay (fixes #2211)
This commit is contained in:
parent
1a54d1f4c6
commit
88674da36a
@ -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.
|
||||
|
@ -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'
|
||||
|
@ -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) {}]);
|
||||
|
||||
|
@ -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',
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
|
@ -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>
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user