Merge pull request #3570 from FinnStutzenstein/markSpeaker
Mark speakers
This commit is contained in:
commit
df523ce526
@ -13,6 +13,7 @@ Agenda:
|
||||
- Fixed issue when sorting a new inserted speaker [#3210].
|
||||
- New permission for managing lists of speakers [#3366].
|
||||
- Fixed multiple request on creation of agenda related items [#3341].
|
||||
- Added possibility to mark speakers [#3570].
|
||||
|
||||
Motions:
|
||||
- New export dialog [#3185].
|
||||
|
20
openslides/agenda/migrations/0004_speaker_marked.py
Normal file
20
openslides/agenda/migrations/0004_speaker_marked.py
Normal 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),
|
||||
),
|
||||
]
|
@ -401,6 +401,11 @@ class Speaker(RESTModelMixin, models.Model):
|
||||
The sort order of the list of speakers. None, if he has already spoken.
|
||||
"""
|
||||
|
||||
marked = models.BooleanField(default=False)
|
||||
"""
|
||||
Marks a speaker.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
|
@ -15,6 +15,7 @@ class SpeakerSerializer(ModelSerializer):
|
||||
'begin_time',
|
||||
'end_time',
|
||||
'weight',
|
||||
'marked',
|
||||
'item', # js-data needs the item-id in the nested object to define relations.
|
||||
)
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
.currentSpeaker {
|
||||
font-weight: bold;
|
||||
|
||||
i {
|
||||
i.fa-microphone {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
@ -42,6 +42,7 @@
|
||||
<ol class="indentation">
|
||||
<li ng-repeat="speaker in lastSpeakers">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
<small class="grey">
|
||||
{{ getDuration(speaker) | osSecondsToTime }} <translate>minutes</translate>
|
||||
(<translate>Start time</translate>:
|
||||
@ -59,10 +60,17 @@
|
||||
<p ng-repeat="speaker in currentSpeaker" class="currentSpeaker spacer indentation">
|
||||
<i class="fa fa-microphone fa-lg"></i>
|
||||
{{ 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()"
|
||||
class="btn btn-default btn-sm" title="{{ 'End speech' | translate }}">
|
||||
<i class="fa fa-microphone-slash"></i> <translate>Stop</translate>
|
||||
</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)"
|
||||
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
|
||||
<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>
|
||||
{{ $index + 1 }}.
|
||||
{{ 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="beginSpeech(speaker.id)"
|
||||
class="btn btn-default btn-sm" title="{{ 'Begin speech' | translate }}">
|
||||
<i class="fa fa-microphone"></i> <translate>Start</translate>
|
||||
</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)"
|
||||
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
|
||||
<i class="fa fa-times"></i>
|
||||
|
@ -7,6 +7,7 @@
|
||||
| limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))"
|
||||
class="lastSpeakers">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</p>
|
||||
|
||||
<!-- Current speaker -->
|
||||
@ -15,6 +16,7 @@
|
||||
<i class="fa fa-microphone fa-lg"></i>
|
||||
<span class="pull-right" style="width:calc(100% - 30px);">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -25,6 +27,7 @@
|
||||
| orderBy:'weight'
|
||||
| limitTo: 3">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</li>
|
||||
</ol>
|
||||
<p ng-if="nextSpeakers.length > 3" class="lastSpeakers">
|
||||
|
@ -18,13 +18,16 @@
|
||||
| limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))"
|
||||
class="lastSpeakers">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</p>
|
||||
|
||||
<!-- Current speaker -->
|
||||
<p ng-repeat="speaker in currentspeakers = (agendaItem.speakers
|
||||
| filter: {end_time: null, begin_time: '!!'})"
|
||||
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>
|
||||
|
||||
<!-- Next speakers -->
|
||||
@ -33,6 +36,7 @@
|
||||
| filter: {begin_time: null}
|
||||
| orderBy:'weight'">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
@ -16,16 +16,20 @@
|
||||
<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">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
|
||||
<!-- Current speaker -->
|
||||
<p ng-repeat="speaker in item.speakers | filter: {end_time: null, begin_time: '!!'}"
|
||||
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 -->
|
||||
<ol class="nextSpeakers">
|
||||
<li ng-repeat="speaker in item.speakers | filter: {begin_time: null} | orderBy:'weight'">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,7 +57,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
result = False
|
||||
return result
|
||||
|
||||
@detail_route(methods=['POST', 'DELETE'])
|
||||
@detail_route(methods=['POST', 'PATCH', 'DELETE'])
|
||||
def manage_speaker(self, request, pk=None):
|
||||
"""
|
||||
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
|
||||
DELETE {'speaker': [<speaker_id>, <speaker_id>, ...]} to remove one or
|
||||
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
|
||||
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.
|
||||
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:
|
||||
# request.method == 'DELETE'
|
||||
speaker_ids = request.data.get('speaker')
|
||||
|
@ -252,6 +252,35 @@ class ManageSpeaker(TestCase):
|
||||
{'speaker': speaker.pk})
|
||||
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):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user