from html import escape from django.contrib.auth import get_user_model from django.db import transaction from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy from reportlab.platypus import Paragraph from openslides.core.config import config from openslides.utils.exceptions import OpenSlidesError from openslides.utils.pdf import stylesheet from openslides.utils.rest_api import ( GenericViewSet, ListModelMixin, Response, RetrieveModelMixin, UpdateModelMixin, ValidationError, detail_route, list_route, ) from openslides.utils.views import PDFView from .models import Item, Speaker from .serializers import ItemSerializer # Viewsets for the REST API class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet): """ API endpoint for agenda items. There are the following views: metadata, list, retrieve, create, partial_update, update, destroy, manage_speaker, speak and tree. """ queryset = Item.objects.all() serializer_class = ItemSerializer def check_view_permissions(self): """ Returns True if the user has required permissions. """ if self.action in ('metadata', 'list', 'retrieve', 'manage_speaker', 'tree'): result = self.request.user.has_perm('agenda.can_see') # For manage_speaker and tree requests the rest of the check is # done in the specific method. See below. elif self.action in ('partial_update', 'update'): result = (self.request.user.has_perm('agenda.can_see') and self.request.user.has_perm('agenda.can_see_hidden_items') and self.request.user.has_perm('agenda.can_manage')) elif self.action in ('speak', 'sort_speakers', 'numbering'): result = (self.request.user.has_perm('agenda.can_see') and self.request.user.has_perm('agenda.can_manage')) else: result = False return result def check_object_permissions(self, request, obj): """ Checks if the requesting user has permission to see also an organizational item if it is one. """ if obj.is_hidden() and not request.user.has_perm('agenda.can_see_hidden_items'): self.permission_denied(request) def get_queryset(self): """ Filters organizational items if the user has no permission to see them. """ queryset = super().get_queryset() if self.request.user.has_perm('agenda.can_see_hidden_items'): return queryset else: return Item.objects.get_only_agenda_items(queryset) @detail_route(methods=['POST', 'DELETE']) def manage_speaker(self, request, pk=None): """ Special view endpoint to add users to the list of speakers or remove them. Send POST {'user': } to add a new speaker. Omit data to add yourself. Send DELETE {'speaker': } to remove someone from the list of speakers. Omit data to remove yourself. Checks also whether the requesting user can do this. He needs at least the permissions 'agenda.can_see' (see self.check_view_permissions()). In case of adding himself the permission 'agenda.can_be_speaker' is required. In case of adding someone else the permission 'agenda.can_manage' is required. In case of removing someone else 'agenda.can_manage' is required. In case of removing himself no other permission is required. """ # Retrieve item. item = self.get_object() if request.method == 'POST': # Retrieve user_id user_id = request.data.get('user') # Check permissions and other conditions. Get user instance. if user_id is None: # Add oneself if not self.request.user.has_perm('agenda.can_be_speaker'): self.permission_denied(request) if item.speaker_list_closed: raise ValidationError({'detail': _('The list of speakers is closed.')}) user = self.request.user else: # Add someone else. if not self.request.user.has_perm('agenda.can_manage'): self.permission_denied(request) try: user = get_user_model().objects.get(pk=int(user_id)) except (ValueError, get_user_model().DoesNotExist): raise ValidationError({'detail': _('User does not exist.')}) # Try to add the user. This ensurse that a user is not twice in the # list of coming speakers. try: Speaker.objects.add(user, item) except OpenSlidesError as e: raise ValidationError({'detail': str(e)}) message = _('User %s was successfully added to the list of speakers.') % user else: # request.method == 'DELETE' # Retrieve speaker_id speaker_id = request.data.get('speaker') # Check permissions and other conditions. Get speaker instance. if speaker_id is None: # Remove oneself queryset = Speaker.objects.filter( item=item, user=self.request.user).exclude(weight=None) 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': _('You are not on the list of speakers.')}) else: # Remove someone else. if not self.request.user.has_perm('agenda.can_manage'): self.permission_denied(request) try: speaker = Speaker.objects.get(pk=int(speaker_id)) except (ValueError, Speaker.DoesNotExist): raise ValidationError({'detail': _('Speaker does not exist.')}) # Delete the speaker. speaker.delete() message = _('Speaker %s was successfully removed from the list of speakers.') % speaker # Initiate response. return Response({'detail': message}) @detail_route(methods=['PUT', 'DELETE']) def speak(self, request, pk=None): """ Special view endpoint to begin and end speech of speakers. Send PUT {'speaker': } 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() if request.method == 'PUT': # Retrieve speaker_id speaker_id = request.data.get('speaker') if speaker_id is None: speaker = item.get_next_speaker() if speaker is None: raise ValidationError({'detail': _('The list of speakers is empty.')}) else: try: speaker = Speaker.objects.get(pk=int(speaker_id)) except (ValueError, Speaker.DoesNotExist): raise ValidationError({'detail': _('Speaker does not exist.')}) 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_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_speech() message = _('The speech is finished now.') # Initiate response. return Response({'detail': message}) @detail_route(methods=['POST']) def sort_speakers(self, request, pk=None): """ Special view endpoint to sort the list of speakers. Expects a list of IDs of the speakers. """ # Retrieve item. item = self.get_object() # Check data speaker_ids = request.data.get('speakers') if not isinstance(speaker_ids, list): raise ValidationError( {'detail': _('Invalid data.')}) # Get all speakers speakers = {} for speaker in item.speakers.filter(begin_time=None): speakers[speaker.pk] = speaker # Check and sort speakers valid_speakers = [] for speaker_id in speaker_ids: if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None: raise ValidationError( {'detail': _('Invalid data.')}) valid_speakers.append(speakers[speaker_id]) weight = 0 with transaction.atomic(): for speaker in valid_speakers: speaker.weight = weight speaker.save() weight += 1 # Initiate response. return Response({'detail': _('List of speakers successfully sorted.')}) @list_route(methods=['get', 'put']) def tree(self, request): """ Returns or sets the agenda tree. """ if request.method == 'PUT': if not (request.user.has_perm('agenda.can_manage') and request.user.has_perm('agenda.can_see_hidden_items')): self.permission_denied(request) try: tree = request.data['tree'] except KeyError as error: response = Response({'detail': 'Agenda tree is missing.'}, status=400) else: try: Item.objects.set_tree(tree) except ValueError as error: response = Response({'detail': str(error)}, status=400) else: response = Response({'detail': 'Agenda tree successfully updated.'}) else: # request.method == 'GET' response = Response(Item.objects.get_tree()) return response @list_route(methods=['post']) def numbering(self, request): """ Auto numbering of the agenda according to the config. Manually added item numbers will be overwritten. """ Item.objects.number_all(numeral_system=config['agenda_numeral_system']) return Response({'detail': _('The agenda has been numbered.')}) # Views to generate PDFs class AgendaPDF(PDFView): """ Create a full agenda-PDF. """ required_permission = 'agenda.can_see' filename = ugettext_lazy('Agenda') document_title = ugettext_lazy('Agenda') def append_to_pdf(self, story): tree = Item.objects.get_tree(only_agenda_items=True, include_content=True) def walk_tree(tree, ancestors=0): """ Generator that yields a two-element-tuple. The first element is an agenda-item and the second a number for steps to the root element. """ for element in tree: yield element['item'], ancestors yield from walk_tree(element['children'], ancestors + 1) for item, ancestors in walk_tree(tree): if ancestors: space = " " * 6 * ancestors story.append(Paragraph( "%s%s" % (space, escape(item.title)), stylesheet['Subitem'])) else: story.append(Paragraph(escape(item.title), stylesheet['Item']))