Merge pull request #1608 from emanuelschuetze/speakerscontrol

Added manage controls for list of speakers of agenda items.
This commit is contained in:
Emanuel Schütze 2015-09-05 11:26:53 +02:00
commit 35fa5470d2
10 changed files with 197 additions and 88 deletions

View File

@ -377,7 +377,7 @@ class Speaker(RESTModelMixin, models.Model):
end_time = models.DateTimeField(null=True)
"""
Saves the time, when the speaker ends his speach. None, if he is not finished yet.
Saves the time, when the speaker ends his speech. None, if he is not finished yet.
"""
weight = models.IntegerField(null=True)
@ -393,19 +393,19 @@ class Speaker(RESTModelMixin, models.Model):
def __str__(self):
return str(self.user)
def begin_speach(self):
def begin_speech(self):
"""
Let the user speak.
Set the weight to None and the time to now. If anyone is still
speaking, end his speach.
speaking, end his speech.
"""
try:
actual_speaker = Speaker.objects.filter(item=self.item, end_time=None).exclude(begin_time=None).get()
except Speaker.DoesNotExist:
pass
else:
actual_speaker.end_speach()
actual_speaker.end_speech()
self.weight = None
self.begin_time = datetime.now()
self.save()
@ -416,9 +416,9 @@ class Speaker(RESTModelMixin, models.Model):
# start_countdown()
pass
def end_speach(self):
def end_speech(self):
"""
The speach is finished. Set the time to now.
The speech is finished. Set the time to now.
"""
self.end_time = datetime.now()
self.save()

View File

@ -81,7 +81,7 @@ class ItemDetailSlide(ProjectorElement):
view_class=ItemViewSet,
view_action='retrieve',
pk=str(item.pk))
for speaker in item.speaker_set.all():
for speaker in item.speakers.all():
yield ProjectorRequirement(
view_class=speaker.user.get_view_class(),
view_action='retrieve',

View File

@ -48,7 +48,7 @@ def setup_agenda_config(sender, **kwargs):
default_value=False,
input_type='boolean',
label=ugettext_lazy('Couple countdown with the list of speakers'),
help_text=ugettext_lazy('[Begin speach] starts the countdown, [End speach] stops the countdown.'),
help_text=ugettext_lazy('[Begin speech] starts the countdown, [End speech] stops the countdown.'),
weight=230,
group=ugettext_lazy('Agenda'))

View File

@ -181,7 +181,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
// close/open list of speakers of current item
$scope.closeList = function (listClosed) {
item.speakerListClosed = listClosed;
item.speaker_list_closed = listClosed;
Agenda.save(item);
};
// add user to list of speakers
@ -198,7 +198,34 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
$scope.removeSpeaker = function (speakerId) {
$http.delete('/rest/agenda/item/' + item.id + '/manage_speaker/',
{headers: {'Content-Type': 'application/json'},
data: JSON.stringify({speaker: speakerId})});
data: JSON.stringify({speaker: speakerId})})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// begin speech of selected/next speaker
$scope.beginSpeech = function (speakerId) {
$http.put('/rest/agenda/item/' + item.id + '/speak/', {'speaker': speakerId})
.success(function(data){
$scope.alert.show = false;
})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// end speech of current speaker
$scope.endSpeech = function () {
$http.delete('/rest/agenda/item/' + item.id + '/speak/',
{headers: {'Content-Type': 'application/json'},
data: JSON.stringify()})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// project list of speakers
$scope.projectListOfSpeakers = function () {
$http.post('/rest/core/projector/1/prune_elements/',
[{name: 'agenda/item', id: item.id, list_of_speakers: true}]);
};
})
@ -301,15 +328,22 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
});
})
.controller('SlideItemDetailCtrl', function($scope, Agenda) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.context.id;
Agenda.find(id);
Agenda.bindOne(id, $scope, 'item');
})
.controller('SlideItemDetailCtrl', [
'$scope',
'Agenda',
'User',
function($scope, Agenda, User) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.context.id;
Agenda.find(id);
User.findAll();
Agenda.bindOne(id, $scope, 'item');
// get flag for list-of-speakers-slide (true/false)
$scope.is_list_of_speakers = $scope.element.context.list_of_speakers;
}
])
.controller('SlideItemListCtrl', function($scope, $http, Agenda) {
// Attention! Each object that is used here has to be dealt on server side.

View File

@ -23,62 +23,114 @@
<div class="white-space-pre-line">{{ item.text }}</div>
<div os-perm="agenda.can_manage">
<h3 os-perm="agenda.can_manage" translate>Duration</h3>
<h2 os-perm="agenda.can_manage" translate>Duration</h2>
{{ item.duration }}
</div>
<div os-perm="agenda.can_manage">
<h3 os-perm="agenda.can_manage" translate>Comment</h3>
<h2 os-perm="agenda.can_manage" translate>Comment</h2>
<div class="white-space-pre-line">{{ item.comment }}</div>
</div>
<h3 translate>List of speakers
<span ng-if="item.speakerListClosed" class="label label-danger" translate>closed</span>
<h2 translate>List of speakers
<span os-perms="agenda.can_manage">
<button ng-if="item.speakerListClosed" ng-click="closeList(false)"
class="btn btn-sm btn-default" translate>
Open list
<button ng-if="item.speaker_list_closed" ng-click="closeList(false)"
class="btn btn-sm btn-danger" translate>
Closed
</button>
<button ng-if="!item.speakerListClosed" ng-click="closeList(true)"
class="btn btn-sm btn-default" translate>
Close list
<button ng-if="!item.speaker_list_closed" ng-click="closeList(true)"
class="btn btn-sm btn-success" translate>
Opened
</button>
</span>
</h3>
<!-- project list -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': item.isProjected() }"
ng-click="projectListOfSpeakers()">
<i class="fa fa-video-camera"></i> Project list
</a>
</h2>
<!-- TODO:
* project list
* show old/current/next speakers
* start/stop speech
* button 'put/remove me on/from the list'
* check permissions
* show only 'add me' OR 'remove me' button
-->
<ol>
<li ng-repeat="speaker in item.speakers">
{{ speaker.user.get_full_name() }}
<button os-perms="agenda.can_manage" ng-click="removeSpeaker(speaker.id)"
class="btn btn-default btn-xs">
<i class="fa fa-times"></i>
</button>
</li>
</ol>
<div class="well">
<button class="btn btn-default btn-xs" type="button"
data-toggle="collapse" data-target="#old_speakers"
aria-expanded="false" aria-controls="collapseExample">
Show all old speakers
</button>
<div os-perms="agenda.can_manage" class="form-group col-sm-6">
<alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">{{ alert.msg }}</alert>
<div class="input-group">
<ui-select ng-model="speaker.selected" ng-change="addSpeaker(speaker.selected.id)">
<ui-select-match placeholder="{{ 'Select or search a participant...' | translate }}">
{{ $select.selected.get_full_name() }}
</ui-select-match>
<ui-select-choices repeat="user in users | filter: $select.search">
<div ng-bind-html="user.get_full_name() | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<span class="input-group-btn">
<a ng-click="speaker={}" class="btn btn-default">
<i class="fa fa-times-circle"></i>
</a>
</span>
<div class="collapse" id="old_speakers">
<h3 translate>Old speakers:</h3>
<ol>
<li ng-repeat="speaker in item.speakers | filter: {end_time: '!!'}">
{{ speaker.user.get_full_name() }}
<small class="grey">
[{{speaker.begin_time | date:'yyyy-MM-dd HH:mm:ss'}}
{{speaker.end_time | date:'yyyy-MM-dd HH:mm:ss'}}]
</small>
</ol>
</div>
<h3 translate>Current speaker:</h3>
<strong ng-repeat="speaker in item.speakers | filter: {end_time: null, begin_time: '!!'}">
{{ speaker.user.get_full_name() }}
</strong>
<h3 translate>Next speakers:</h3>
<ol>
<li ng-repeat="speaker in item.speakers | filter: {begin_time: null}">
{{ speaker.user.get_full_name() }}
<button os-perms="agenda.can_manage" ng-click="removeSpeaker(speaker.id)"
class="btn btn-default btn-xs">
<i class="fa fa-times"></i>
</button>
<button os-perms="agenda.can_manage" ng-click="beginSpeech(speaker.id)"
class="btn btn-default btn-xs">
<i class="fa fa-play"></i>
</button>
</ol>
<div class="form-group">
<alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
{{alert.msg}}
</alert>
<div os-perms="agenda.can_manage" class="input-group">
<ui-select ng-model="speaker.selected" ng-change="addSpeaker(speaker.selected.id)">
<ui-select-match placeholder="{{ 'Select or search a participant...' | translate }}">
{{ $select.selected.get_full_name() }}
</ui-select-match>
<ui-select-choices repeat="user in users | filter: $select.search">
<div ng-bind-html="user.get_full_name() | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<span class="input-group-btn">
<a ng-click="speaker={}" class="btn btn-default">
<i class="fa fa-times-circle"></i>
</a>
</span>
</div>
<p os-perm="agenda.can_be_speaker">
<button ng-click="addSpeaker()" class="btn btn-default">
<i class="fa fa-plus"></i>
<translate>Add me</translate>
</button>
<button ng-click="removeSpeaker()" class="btn btn-default">
<i class="fa fa-minus"></i>
<translate>Remove me</translate>
</button>
<p os-perms="agenda.can_manage">
<button ng-click="beginSpeech()"
class="btn btn-primary">
<i class="fa fa-play"></i>
<translate>Start next speaker</translate>
</button>
<button ng-click="endSpeech()"
class="btn btn-default">
<i class="fa fa-stop"></i>
<translate>Stop current speaker</translate>
</button>
</div>
</div>

View File

@ -1,4 +1,27 @@
<div ng-controller="SlideItemDetailCtrl" class="content">
<h1>{{ item.title }}</h1>
<div class="white-space-pre-line">{{ item.text }}</div>
<h1>
{{ item.title }}
<small ng-if="is_list_of_speakers">
<span translate>List of speakers</span>
<span ng-if="item.speaker_list_closed" class="label label-danger" translate>Closed</span>
</small>
</h1>
<!-- Item text -->
<div ng-if="!is_list_of_speakers" class="white-space-pre-line">{{ item.text }}</div>
<!-- List of speakers -->
<div ng-if="is_list_of_speakers">
<!-- TODO: show last old speakers -->
<!-- current speaker -->
<p>
<strong ng-repeat="speaker in item.speakers | filter: {end_time: null, begin_time: '!!'}">
{{ speaker.user.get_full_name() }}
</strong>
<!-- next speakers -->
<ol id="list_of_speakers">
<li ng-repeat="speaker in item.speakers | filter: {begin_time: null}">
{{ speaker.user.get_full_name() }}
</ol>
</div>
</div>

View File

@ -152,9 +152,9 @@ class ItemViewSet(ModelViewSet):
@detail_route(methods=['PUT', 'DELETE'])
def speak(self, request, pk=None):
"""
Special view endpoint to begin and end speach of speakers. Send PUT
{'speaker': <speaker_id>} to begin speach. Omit data to begin speach of
the next speaker. Send DELETE to end speach of current speaker.
Special view endpoint to begin and end speech of speakers. Send PUT
{'speaker': <speaker_id>} to begin speech. Omit data to begin speech of
the next speaker. Send DELETE to end speech of current speaker.
"""
# Retrieve item.
item = self.get_object()
@ -171,21 +171,21 @@ class ItemViewSet(ModelViewSet):
speaker = Speaker.objects.get(pk=int(speaker_id))
except (ValueError, Speaker.DoesNotExist):
raise ValidationError({'detail': _('Speaker does not exist.')})
speaker.begin_speach()
speaker.begin_speech()
message = _('User is now speaking.')
else:
# request.method == 'DELETE'
try:
# We assume that there aren't multiple entries because this
# is forbidden by the Model's begin_speach method. We assume that
# is forbidden by the Model's begin_speech method. We assume that
# there is only one speaker instance or none.
current_speaker = Speaker.objects.filter(item=item, end_time=None).exclude(begin_time=None).get()
except Speaker.DoesNotExist:
raise ValidationError(
{'detail': _('There is no one speaking at the moment according to %(item)s.') % {'item': item}})
current_speaker.end_speach()
message = _('The speach is finished now.')
current_speaker.end_speech()
message = _('The speech is finished now.')
# Initiate response.
return Response({'detail': message})

View File

@ -125,7 +125,7 @@ class ManageSpeaker(TestCase):
class Speak(TestCase):
"""
Tests view to begin or end speach.
Tests view to begin or end speech.
"""
def setUp(self):
self.client = APIClient()
@ -135,7 +135,7 @@ class Speak(TestCase):
username='test_user_Aigh4vohb3seecha4aa4',
password='test_password_eneupeeVo5deilixoo8j')
def test_begin_speach(self):
def test_begin_speech(self):
Speaker.objects.add(self.user, self.item)
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
self.assertTrue(Speaker.objects.get(pk=speaker.pk).begin_time is None)
@ -145,7 +145,7 @@ class Speak(TestCase):
self.assertEqual(response.status_code, 200)
self.assertFalse(Speaker.objects.get(pk=speaker.pk).begin_time is None)
def test_begin_speach_next_speaker(self):
def test_begin_speech_next_speaker(self):
speaker = Speaker.objects.add(self.user, self.item)
Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
@ -154,27 +154,27 @@ class Speak(TestCase):
self.assertEqual(response.status_code, 200)
self.assertFalse(Speaker.objects.get(pk=speaker.pk).begin_time is None)
def test_begin_speach_invalid_speaker_id(self):
def test_begin_speech_invalid_speaker_id(self):
response = self.client.put(
reverse('item-speak', args=[self.item.pk]),
{'speaker': '1'})
self.assertEqual(response.status_code, 400)
def test_begin_speach_invalid_data(self):
def test_begin_speech_invalid_data(self):
response = self.client.put(
reverse('item-speak', args=[self.item.pk]),
{'speaker': 'invalid'})
self.assertEqual(response.status_code, 400)
def test_end_speach(self):
def test_end_speech(self):
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
speaker.begin_speach()
speaker.begin_speech()
self.assertFalse(Speaker.objects.get(pk=speaker.pk).begin_time is None)
self.assertTrue(Speaker.objects.get(pk=speaker.pk).end_time is None)
response = self.client.delete(reverse('item-speak', args=[self.item.pk]))
self.assertEqual(response.status_code, 200)
self.assertFalse(Speaker.objects.get(pk=speaker.pk).end_time is None)
def test_end_speach_no_current_speaker(self):
def test_end_speech_no_current_speaker(self):
response = self.client.delete(reverse('item-speak', args=[self.item.pk]))
self.assertEqual(response.status_code, 400)

View File

@ -46,18 +46,18 @@ class ListOfSpeakerModelTests(TestCase):
speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1)
self.assertIsNone(speaker1_item1.begin_time)
self.assertIsNone(speaker1_item1.end_time)
speaker1_item1.begin_speach()
speaker1_item1.begin_speech()
self.assertIsNotNone(Speaker.objects.get(pk=speaker1_item1.pk).begin_time)
self.assertIsNone(Speaker.objects.get(pk=speaker1_item1.pk).weight)
speaker1_item1.end_speach()
speaker1_item1.end_speech()
self.assertIsNotNone(Speaker.objects.get(pk=speaker1_item1.pk).end_time)
def test_finish_when_other_speaker_begins(self):
speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1)
speaker2_item1 = Speaker.objects.add(self.speaker2, self.item1)
speaker1_item1.begin_speach()
speaker1_item1.begin_speech()
self.assertIsNone(speaker1_item1.end_time)
self.assertIsNone(speaker2_item1.begin_time)
speaker2_item1.begin_speach()
speaker2_item1.begin_speech()
self.assertIsNotNone(Speaker.objects.get(user=self.speaker1, item=self.item1).end_time)
self.assertIsNotNone(speaker2_item1.begin_time)

View File

@ -56,7 +56,7 @@ class ItemViewSetManageSpeaker(TestCase):
class ItemViewSetSpeak(TestCase):
"""
Tests views of ItemViewSet to begin and end speach.
Tests views of ItemViewSet to begin and end speech.
"""
def setUp(self):
self.request = MagicMock()
@ -65,27 +65,27 @@ class ItemViewSetSpeak(TestCase):
self.view_instance.get_object = get_object_mock = MagicMock()
get_object_mock.return_value = self.mock_item = MagicMock()
def test_begin_speach(self):
def test_begin_speech(self):
self.request.method = 'PUT'
self.request.user.has_perm.return_value = True
self.request.data = {}
self.mock_item.get_next_speaker.return_value = mock_next_speaker = MagicMock()
self.view_instance.speak(self.request)
mock_next_speaker.begin_speach.assert_called_with()
mock_next_speaker.begin_speech.assert_called_with()
@patch('openslides.agenda.views.Speaker')
def test_begin_speach_specific_speaker(self, mock_speaker):
def test_begin_speech_specific_speaker(self, mock_speaker):
self.request.method = 'PUT'
self.request.user.has_perm.return_value = True
self.request.data = {'speaker': '1'}
mock_speaker.objects.get.return_value = mock_next_speaker = MagicMock()
self.view_instance.speak(self.request)
mock_next_speaker.begin_speach.assert_called_with()
mock_next_speaker.begin_speech.assert_called_with()
@patch('openslides.agenda.views.Speaker')
def test_end_speach(self, mock_speaker):
def test_end_speech(self, mock_speaker):
self.request.method = 'DELETE'
self.request.user.has_perm.return_value = True
mock_speaker.objects.filter.return_value.exclude.return_value.get.return_value = mock_speaker = MagicMock()
self.view_instance.speak(self.request)
mock_speaker.end_speach.assert_called_with()
mock_speaker.end_speech.assert_called_with()