347 lines
14 KiB
Python
347 lines
14 KiB
Python
from django.contrib.auth import get_user_model
|
|
from django.db import transaction
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from openslides.core.config import config
|
|
from openslides.utils.autoupdate import inform_changed_data
|
|
from openslides.utils.exceptions import OpenSlidesError
|
|
from openslides.utils.rest_api import (
|
|
GenericViewSet,
|
|
ListModelMixin,
|
|
Response,
|
|
RetrieveModelMixin,
|
|
UpdateModelMixin,
|
|
ValidationError,
|
|
detail_route,
|
|
list_route,
|
|
)
|
|
from openslides.utils.views import BinaryTemplateView
|
|
|
|
from ..utils.auth import has_perm
|
|
from .access_permissions import ItemAccessPermissions
|
|
from .models import Item, Speaker
|
|
|
|
|
|
# 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.
|
|
"""
|
|
access_permissions = ItemAccessPermissions()
|
|
queryset = Item.objects.all()
|
|
|
|
def check_view_permissions(self):
|
|
"""
|
|
Returns True if the user has required permissions.
|
|
"""
|
|
if self.action in ('list', 'retrieve'):
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
elif self.action in ('metadata', 'manage_speaker', 'tree'):
|
|
result = has_perm(self.request.user, '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 = (has_perm(self.request.user, 'agenda.can_see') and
|
|
has_perm(self.request.user, 'agenda.can_see_hidden_items') and
|
|
has_perm(self.request.user, 'agenda.can_manage'))
|
|
elif self.action in ('speak', 'sort_speakers'):
|
|
result = (has_perm(self.request.user, 'agenda.can_see') and
|
|
has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'))
|
|
elif self.action in ('numbering', 'sort'):
|
|
result = (has_perm(self.request.user, 'agenda.can_see') and
|
|
has_perm(self.request.user, 'agenda.can_manage'))
|
|
else:
|
|
result = False
|
|
return result
|
|
|
|
def update(self, *args, **kwargs):
|
|
"""
|
|
Customized view endpoint to update all children if, the item type has changed.
|
|
"""
|
|
hidden = self.get_object().type == Item.HIDDEN_ITEM
|
|
|
|
result = super().update(*args, **kwargs)
|
|
|
|
# update all children, if the item type has changed
|
|
item = self.get_object()
|
|
if hidden != (item.type == Item.HIDDEN_ITEM):
|
|
items_to_update = []
|
|
|
|
# rekursively add children to items_to_update
|
|
def add_item(item):
|
|
items_to_update.append(item)
|
|
for child in item.children.all():
|
|
add_item(child)
|
|
|
|
add_item(item)
|
|
inform_changed_data(items_to_update)
|
|
|
|
return result
|
|
|
|
@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
|
|
them. Send POST {'user': <user_id>} to add a new speaker. Omit
|
|
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
|
|
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 has_perm(self.request.user, '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 has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
|
|
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
|
|
|
|
# Send new speaker via autoupdate because users without permission
|
|
# 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')
|
|
|
|
# Check permissions and other conditions. Get speaker instance.
|
|
if speaker_ids 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:
|
|
speaker.delete()
|
|
message = _('You are successfully removed from the list of speakers.')
|
|
else:
|
|
# Remove someone else.
|
|
if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
|
|
self.permission_denied(request)
|
|
if type(speaker_ids) is int:
|
|
speaker_ids = [speaker_ids]
|
|
deleted_speaker_count = 0
|
|
for speaker_id in speaker_ids:
|
|
try:
|
|
speaker = Speaker.objects.get(pk=int(speaker_id))
|
|
except (ValueError, Speaker.DoesNotExist):
|
|
pass
|
|
else:
|
|
speaker.delete(skip_autoupdate=True)
|
|
deleted_speaker_name = speaker
|
|
deleted_speaker_count += 1
|
|
# send autoupdate if speakers are deleted
|
|
if deleted_speaker_count > 0:
|
|
inform_changed_data(item)
|
|
|
|
if deleted_speaker_count > 1:
|
|
message = str(deleted_speaker_count) + ' ' + _('speakers have been removed from the list of speakers.')
|
|
elif deleted_speaker_count == 1:
|
|
message = _('User %s has been removed from the list of speakers.') % deleted_speaker_name
|
|
else:
|
|
message = _('No speakers have been removed from the list of speakers.')
|
|
# 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': <speaker_id>} 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(skip_autoupdate=True)
|
|
weight += 1
|
|
|
|
# send autoupdate
|
|
inform_changed_data(item)
|
|
|
|
# Initiate response.
|
|
return Response({'detail': _('List of speakers successfully sorted.')})
|
|
|
|
@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.')})
|
|
|
|
@list_route(methods=['post'])
|
|
def sort(self, request):
|
|
"""
|
|
Sort agenda items. Also checks parent field to prevent hierarchical
|
|
loops.
|
|
"""
|
|
nodes = request.data.get('nodes', [])
|
|
parent_id = request.data.get('parent_id')
|
|
items = []
|
|
with transaction.atomic():
|
|
for index, node in enumerate(nodes):
|
|
item = Item.objects.get(pk=node['id'])
|
|
item.parent_id = parent_id
|
|
item.weight = index
|
|
item.save(skip_autoupdate=True)
|
|
items.append(item)
|
|
|
|
# Now check consistency. TODO: Try to use less DB queries.
|
|
item = Item.objects.get(pk=node['id'])
|
|
ancestor = item.parent
|
|
while ancestor is not None:
|
|
if ancestor == item:
|
|
raise ValidationError({'detail': _(
|
|
'There must not be a hierarchical loop. Please reload the page.')})
|
|
ancestor = ancestor.parent
|
|
|
|
inform_changed_data(items)
|
|
return Response({'detail': _('The agenda has been sorted.')})
|
|
|
|
|
|
# Special views
|
|
class AgendaDocxTemplateView(BinaryTemplateView):
|
|
"""
|
|
Returns the template for motions docx export
|
|
"""
|
|
template_name = 'templates/docx/agenda.docx'
|