diff --git a/CHANGELOG b/CHANGELOG index a350f4488..a5cfacd01 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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]. diff --git a/openslides/agenda/migrations/0004_speaker_marked.py b/openslides/agenda/migrations/0004_speaker_marked.py new file mode 100644 index 000000000..5bc19fccf --- /dev/null +++ b/openslides/agenda/migrations/0004_speaker_marked.py @@ -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), + ), + ] diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index de3247321..8f252bb8c 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -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 = ( diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py index fb43a7615..259a3489f 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -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. ) diff --git a/openslides/agenda/static/css/agenda/_list_of_speakers.scss b/openslides/agenda/static/css/agenda/_list_of_speakers.scss index 81c5c34da..2661ed101 100644 --- a/openslides/agenda/static/css/agenda/_list_of_speakers.scss +++ b/openslides/agenda/static/css/agenda/_list_of_speakers.scss @@ -7,7 +7,7 @@ .currentSpeaker { font-weight: bold; - i { + i.fa-microphone { padding-right: 5px; } } diff --git a/openslides/agenda/static/js/agenda/site.js b/openslides/agenda/static/js/agenda/site.js index 310200866..262908fd8 100644 --- a/openslides/agenda/static/js/agenda/site.js +++ b/openslides/agenda/static/js/agenda/site.js @@ -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); + }); + }; } ]) diff --git a/openslides/agenda/static/templates/agenda/list-of-speakers-partial-management.html b/openslides/agenda/static/templates/agenda/list-of-speakers-partial-management.html index a96f08af8..ecfaf7b7b 100644 --- a/openslides/agenda/static/templates/agenda/list-of-speakers-partial-management.html +++ b/openslides/agenda/static/templates/agenda/list-of-speakers-partial-management.html @@ -42,6 +42,7 @@
  1. {{ speaker.user.get_full_name() }} + {{ getDuration(speaker) | osSecondsToTime }} minutes (Start time: @@ -59,10 +60,17 @@

    {{ speaker.user.get_full_name() }} + + + + +

diff --git a/openslides/agenda/static/templates/agenda/partial-slide-current-list-of-speakers.html b/openslides/agenda/static/templates/agenda/partial-slide-current-list-of-speakers.html index ebd4f1c87..18d2bf1b7 100644 --- a/openslides/agenda/static/templates/agenda/partial-slide-current-list-of-speakers.html +++ b/openslides/agenda/static/templates/agenda/partial-slide-current-list-of-speakers.html @@ -18,13 +18,16 @@ | 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() }} + + {{ speaker.user.get_full_name() }} +

@@ -33,6 +36,7 @@ | filter: {begin_time: null} | orderBy:'weight'"> {{ speaker.user.get_full_name() }} + diff --git a/openslides/agenda/static/templates/agenda/slide-list-of-speakers.html b/openslides/agenda/static/templates/agenda/slide-list-of-speakers.html index 07cf282b1..a953feeb1 100644 --- a/openslides/agenda/static/templates/agenda/slide-list-of-speakers.html +++ b/openslides/agenda/static/templates/agenda/slide-list-of-speakers.html @@ -16,16 +16,20 @@

{{ speaker.user.get_full_name() }} +

- {{ speaker.user.get_full_name() }} + + {{ speaker.user.get_full_name() }} +

  1. {{ speaker.user.get_full_name() }} +
diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index edc628aab..94bf898da 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -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': } or DELETE {'speaker': [, , ...]} to remove one or more speakers from the list of speakers. Omit data to remove yourself. + Send PATCH {'user': , 'marked': } 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') diff --git a/tests/integration/agenda/test_viewset.py b/tests/integration/agenda/test_viewset.py index bc4331732..509c308e9 100644 --- a/tests/integration/agenda/test_viewset.py +++ b/tests/integration/agenda/test_viewset.py @@ -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): """