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].
|
- 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].
|
||||||
|
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.
|
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 = (
|
||||||
|
@ -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.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
.currentSpeaker {
|
.currentSpeaker {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
i {
|
i.fa-microphone {
|
||||||
padding-right: 5px;
|
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">
|
<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>
|
||||||
|
|
||||||
<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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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')
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user