Merge pull request #3570 from FinnStutzenstein/markSpeaker

Mark speakers
This commit is contained in:
Emanuel Schütze 2018-02-14 10:24:36 +01:00 committed by GitHub
commit df523ce526
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 132 additions and 4 deletions

View File

@ -13,6 +13,7 @@ Agenda:
- Fixed issue when sorting a new inserted speaker [#3210]. - Fixed issue when sorting a new inserted speaker [#3210].
- New permission for managing lists of speakers [#3366]. - New permission for managing lists of speakers [#3366].
- Fixed multiple request on creation of agenda related items [#3341]. - Fixed multiple request on creation of agenda related items [#3341].
- Added possibility to mark speakers [#3570].
Motions: Motions:
- New export dialog [#3185]. - New export dialog [#3185].

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-02-06 12:26
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda', '0003_auto_20170818_1202'),
]
operations = [
migrations.AddField(
model_name='speaker',
name='marked',
field=models.BooleanField(default=False),
),
]

View File

@ -401,6 +401,11 @@ class Speaker(RESTModelMixin, models.Model):
The sort order of the list of speakers. None, if he has already spoken. The sort order of the list of speakers. None, if he has already spoken.
""" """
marked = models.BooleanField(default=False)
"""
Marks a speaker.
"""
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (

View File

@ -15,6 +15,7 @@ class SpeakerSerializer(ModelSerializer):
'begin_time', 'begin_time',
'end_time', 'end_time',
'weight', 'weight',
'marked',
'item', # js-data needs the item-id in the nested object to define relations. 'item', # js-data needs the item-id in the nested object to define relations.
) )

View File

@ -7,7 +7,7 @@
.currentSpeaker { .currentSpeaker {
font-weight: bold; font-weight: bold;
i { i.fa-microphone {
padding-right: 5px; padding-right: 5px;
} }
} }

View File

@ -569,6 +569,18 @@ angular.module('OpenSlidesApp.agenda.site', [
); );
} }
}; };
// Marking a speaker
$scope.toggleMarked = function (speaker) {
$http.patch('/rest/agenda/item/' + $scope.item.id + '/manage_speaker/', {
user: speaker.user.id,
marked: !speaker.marked,
}).then(function (success) {
$scope.alert.show = false;
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
});
};
} }
]) ])

View File

@ -42,6 +42,7 @@
<ol class="indentation"> <ol class="indentation">
<li ng-repeat="speaker in lastSpeakers"> <li ng-repeat="speaker in lastSpeakers">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
<small class="grey"> <small class="grey">
{{ getDuration(speaker) | osSecondsToTime }} <translate>minutes</translate> {{ getDuration(speaker) | osSecondsToTime }} <translate>minutes</translate>
(<translate>Start time</translate>: (<translate>Start time</translate>:
@ -59,10 +60,17 @@
<p ng-repeat="speaker in currentSpeaker" class="currentSpeaker spacer indentation"> <p ng-repeat="speaker in currentSpeaker" class="currentSpeaker spacer indentation">
<i class="fa fa-microphone fa-lg"></i> <i class="fa fa-microphone fa-lg"></i>
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<span os-perms="!agenda.can_manage_list_of_speakers">
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</span>
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="endSpeech()" <button os-perms="agenda.can_manage_list_of_speakers" ng-click="endSpeech()"
class="btn btn-default btn-sm" title="{{ 'End speech' | translate }}"> class="btn btn-default btn-sm" title="{{ 'End speech' | translate }}">
<i class="fa fa-microphone-slash"></i> <translate>Stop</translate> <i class="fa fa-microphone-slash"></i> <translate>Stop</translate>
</button> </button>
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="toggleMarked(speaker)"
class="btn btn-default btn-sm" title="{{ 'Mark speaker' | translate }}">
<i class="fa" ng-class="speaker.marked ? 'fa-star' : 'fa-star-o'"></i>
</button>
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="removeSpeaker(speaker.id)" <button os-perms="agenda.can_manage_list_of_speakers" ng-click="removeSpeaker(speaker.id)"
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}"> class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
@ -77,11 +85,18 @@
<i os-perms="agenda.can_manage_list_of_speakers" ui-tree-handle="" class="fa fa-arrows-v"></i> <i os-perms="agenda.can_manage_list_of_speakers" ui-tree-handle="" class="fa fa-arrows-v"></i>
{{ $index + 1 }}. {{ $index + 1 }}.
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<span os-perms="!agenda.can_manage_list_of_speakers">
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</span>
&nbsp; &nbsp;
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="beginSpeech(speaker.id)" <button os-perms="agenda.can_manage_list_of_speakers" ng-click="beginSpeech(speaker.id)"
class="btn btn-default btn-sm" title="{{ 'Begin speech' | translate }}"> class="btn btn-default btn-sm" title="{{ 'Begin speech' | translate }}">
<i class="fa fa-microphone"></i> <translate>Start</translate> <i class="fa fa-microphone"></i> <translate>Start</translate>
</button> </button>
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="toggleMarked(speaker)"
class="btn btn-default btn-sm" title="{{ 'Mark speaker' | translate }}">
<i class="fa" ng-class="speaker.marked ? 'fa-star' : 'fa-star-o'"></i>
</button>
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="removeSpeaker(speaker.id)" <button os-perms="agenda.can_manage_list_of_speakers" ng-click="removeSpeaker(speaker.id)"
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}"> class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>

View File

@ -7,6 +7,7 @@
| limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" | limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))"
class="lastSpeakers"> class="lastSpeakers">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</p> </p>
<!-- Current speaker --> <!-- Current speaker -->
@ -15,6 +16,7 @@
<i class="fa fa-microphone fa-lg"></i> <i class="fa fa-microphone fa-lg"></i>
<span class="pull-right" style="width:calc(100% - 30px);"> <span class="pull-right" style="width:calc(100% - 30px);">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</span> </span>
</div> </div>
@ -25,6 +27,7 @@
| orderBy:'weight' | orderBy:'weight'
| limitTo: 3"> | limitTo: 3">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</li> </li>
</ol> </ol>
<p ng-if="nextSpeakers.length > 3" class="lastSpeakers"> <p ng-if="nextSpeakers.length > 3" class="lastSpeakers">

View File

@ -18,13 +18,16 @@
| limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" | limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))"
class="lastSpeakers"> class="lastSpeakers">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</p> </p>
<!-- Current speaker --> <!-- Current speaker -->
<p ng-repeat="speaker in currentspeakers = (agendaItem.speakers <p ng-repeat="speaker in currentspeakers = (agendaItem.speakers
| filter: {end_time: null, begin_time: '!!'})" | filter: {end_time: null, begin_time: '!!'})"
class="currentSpeaker nobr"> class="currentSpeaker nobr">
<i class="fa fa-microphone fa-lg"></i> {{ speaker.user.get_full_name() }} <i class="fa fa-microphone fa-lg"></i>
{{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</p> </p>
<!-- Next speakers --> <!-- Next speakers -->
@ -33,6 +36,7 @@
| filter: {begin_time: null} | filter: {begin_time: null}
| orderBy:'weight'"> | orderBy:'weight'">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</li> </li>
</ol> </ol>
</div> </div>

View File

@ -16,16 +16,20 @@
<p ng-repeat="speaker in lastSpeakers = (item.speakers | filter: {end_time: '!!', begin_time: '!!'}) | <p ng-repeat="speaker in lastSpeakers = (item.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers"> limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
<!-- Current speaker --> <!-- Current speaker -->
<p ng-repeat="speaker in item.speakers | filter: {end_time: null, begin_time: '!!'}" <p ng-repeat="speaker in item.speakers | filter: {end_time: null, begin_time: '!!'}"
class="currentSpeaker"> class="currentSpeaker">
<i class="fa fa-microphone fa-lg"></i> {{ speaker.user.get_full_name() }} <i class="fa fa-microphone fa-lg"></i>
{{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
<!-- Next speakers --> <!-- Next speakers -->
<ol class="nextSpeakers"> <ol class="nextSpeakers">
<li ng-repeat="speaker in item.speakers | filter: {begin_time: null} | orderBy:'weight'"> <li ng-repeat="speaker in item.speakers | filter: {begin_time: null} | orderBy:'weight'">
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
</ol> </ol>
</div> </div>
</div> </div>

View File

@ -57,7 +57,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
result = False result = False
return result return result
@detail_route(methods=['POST', 'DELETE']) @detail_route(methods=['POST', 'PATCH', 'DELETE'])
def manage_speaker(self, request, pk=None): def manage_speaker(self, request, pk=None):
""" """
Special view endpoint to add users to the list of speakers or remove Special view endpoint to add users to the list of speakers or remove
@ -65,6 +65,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
data to add yourself. Send DELETE {'speaker': <speaker_id>} or data to add yourself. Send DELETE {'speaker': <speaker_id>} or
DELETE {'speaker': [<speaker_id>, <speaker_id>, ...]} to remove one or DELETE {'speaker': [<speaker_id>, <speaker_id>, ...]} to remove one or
more speakers from the list of speakers. Omit data to remove yourself. more speakers from the list of speakers. Omit data to remove yourself.
Send PATCH {'user': <user_id>, 'marked': <bool>} to mark the speaker.
Checks also whether the requesting user can do this. He needs at Checks also whether the requesting user can do this. He needs at
least the permissions 'agenda.can_see' (see least the permissions 'agenda.can_see' (see
@ -110,6 +111,39 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
# to see users may not have it but can get it now. # to see users may not have it but can get it now.
inform_changed_data([user]) inform_changed_data([user])
# Toggle 'marked' for the speaker
elif request.method == 'PATCH':
# Check permissions
if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
self.permission_denied(request)
# Retrieve user_id
user_id = request.data.get('user')
try:
user = get_user_model().objects.get(pk=int(user_id))
except (ValueError, get_user_model().DoesNotExist):
raise ValidationError({'detail': _('User does not exist.')})
marked = request.data.get('marked')
if not isinstance(marked, bool):
raise ValidationError({'detail': _('Marked has to be a bool.')})
queryset = Speaker.objects.filter(item=item, user=user)
try:
# We assume that there aren't multiple entries because this
# is forbidden by the Manager's add method. We assume that
# there is only one speaker instance or none.
speaker = queryset.get()
except Speaker.DoesNotExist:
raise ValidationError({'detail': _('The user is not in the list of speakers.')})
else:
speaker.marked = marked
speaker.save()
if speaker.marked:
message = _('You are successfully marked the speaker.')
else:
message = _('You are successfully unmarked the speaker.')
else: else:
# request.method == 'DELETE' # request.method == 'DELETE'
speaker_ids = request.data.get('speaker') speaker_ids = request.data.get('speaker')

View File

@ -252,6 +252,35 @@ class ManageSpeaker(TestCase):
{'speaker': speaker.pk}) {'speaker': speaker.pk})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_mark_speaker(self):
Speaker.objects.add(self.user, self.item)
response = self.client.patch(
reverse('item-manage-speaker', args=[self.item.pk]),
{
'user': self.user.pk,
'marked': True,
},
format='json'
)
self.assertEqual(response.status_code, 200)
self.assertTrue(Speaker.objects.get().marked)
def test_mark_speaker_non_admin(self):
admin = get_user_model().objects.get(username='admin')
group_staff = admin.groups.get(name='Staff')
group_delegates = type(group_staff).objects.get(name='Delegates')
admin.groups.add(group_delegates)
admin.groups.remove(group_staff)
CollectionElement.from_instance(admin)
Speaker.objects.add(self.user, self.item)
response = self.client.patch(
reverse('item-manage-speaker', args=[self.item.pk]),
{'user': self.user.pk})
self.assertEqual(response.status_code, 403)
class Speak(TestCase): class Speak(TestCase):
""" """