Merge pull request #2489 from matakuka/issue2211-multiprojector
list-of-speakers overlay (issue #2211, Version 4)
This commit is contained in:
commit
ac60fb53da
@ -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.
|
||||||
|
@ -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
|
|
||||||
|
@ -298,7 +298,7 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
|
|||||||
'Agenda',
|
'Agenda',
|
||||||
function (Projector, Assignment, Topic, Motion, Agenda) {
|
function (Projector, Assignment, Topic, Motion, Agenda) {
|
||||||
return {
|
return {
|
||||||
getItem: function (projectorId) {
|
getItem: function (projectorId) {
|
||||||
var elementPromise;
|
var elementPromise;
|
||||||
return Projector.find(projectorId).then(function (projector) {
|
return Projector.find(projectorId).then(function (projector) {
|
||||||
// scan all elements
|
// 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.
|
// Make sure that the Agenda resource is loaded.
|
||||||
.run(['Agenda', function(Agenda) {}]);
|
.run(['Agenda', function(Agenda) {}]);
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -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')):
|
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
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
Loading…
Reference in New Issue
Block a user