2018-11-27 22:35:19 +01:00
|
|
|
import jsonschema
|
2015-05-26 18:21:30 +02:00
|
|
|
from django.contrib.auth import get_user_model
|
2015-12-19 00:29:20 +01:00
|
|
|
from django.db import transaction
|
2019-06-03 17:04:30 +02:00
|
|
|
from django.db.utils import IntegrityError
|
2021-04-22 11:25:27 +02:00
|
|
|
from django.http.request import QueryDict
|
2012-02-20 17:46:45 +01:00
|
|
|
|
2015-11-25 10:48:33 +01:00
|
|
|
from openslides.core.config import config
|
2016-12-06 12:21:29 +01:00
|
|
|
from openslides.utils.autoupdate import inform_changed_data
|
2015-05-26 18:21:30 +02:00
|
|
|
from openslides.utils.exceptions import OpenSlidesError
|
|
|
|
from openslides.utils.rest_api import (
|
2015-10-24 19:02:43 +02:00
|
|
|
GenericViewSet,
|
2019-04-23 16:57:35 +02:00
|
|
|
ModelViewSet,
|
2015-05-26 18:21:30 +02:00
|
|
|
Response,
|
2015-10-24 19:02:43 +02:00
|
|
|
UpdateModelMixin,
|
2015-05-26 18:21:30 +02:00
|
|
|
ValidationError,
|
2021-04-12 08:20:06 +02:00
|
|
|
action,
|
2019-06-03 17:04:30 +02:00
|
|
|
status,
|
2015-05-26 18:21:30 +02:00
|
|
|
)
|
Replaces the old `angular2tree` with a custom drag&drop tree
Calculates the direction of the moving.
Finishes the moving of nodes in same level
Adds some style
Sets the padding dynamically
Adds placeholder depends on the horizontal movement
Set the placeholder at the correct place, so the user can see, where he will drop the moved node
Finishes moving of nodes
- Old parents change their option to expand.
- New parents change their option to expand.
- If the user moves a node between nodes with a higher level, the node will be moved to the next index with same or lower level.
Fixes the visibility of moved node
- If the new parent is not visible, the moved node will not be seen.
If the user moves an expanded node, the new parent should expanded, too, if it's not already.
Sending successfully data to the server
- Sorting the items
Handles moving nodes between parent and children
- If the user moves a node between a parent and its children, the children will be relinked to the moved node as their new parent.
Replaces the old `sorting-tree` to a new one
- The new `sorted-tree` replaces the old `sorting-tree`.
- The old package `angular-tree-component` was removed.
- The user will only see the buttons to save or cancel his changes, if he made changes.
- The buttons, that do not work currently, were removed.
Adds a guard to check if the user made changes.
- If the user made changes but he has not saved them, then there is a dialog that will prompt to ask for confirmation.
Before cancelling the changes the user has to confirm this.
2019-02-22 12:04:36 +01:00
|
|
|
from openslides.utils.views import TreeSortMixin
|
2014-03-27 20:30:15 +01:00
|
|
|
|
2019-03-06 14:53:24 +01:00
|
|
|
from ..utils.auth import has_perm
|
2019-06-03 17:04:30 +02:00
|
|
|
from ..utils.utils import get_model_from_collection_string
|
2019-04-23 16:57:35 +02:00
|
|
|
from .models import Item, ListOfSpeakers, Speaker
|
2012-02-20 17:46:45 +01:00
|
|
|
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
# Viewsets for the REST API
|
2013-02-16 10:41:22 +01:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2019-04-23 16:57:35 +02:00
|
|
|
class ItemViewSet(ModelViewSet, TreeSortMixin):
|
2015-01-06 00:14:49 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for agenda items.
|
|
|
|
|
2018-11-27 22:35:19 +01:00
|
|
|
There are some views, see check_view_permissions.
|
2015-01-06 00:14:49 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2015-02-12 18:48:14 +01:00
|
|
|
queryset = Item.objects.all()
|
2015-01-06 00:14:49 +01:00
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
def check_view_permissions(self):
|
2015-01-06 00:14:49 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns True if the user has required permissions.
|
2015-01-06 00:14:49 +01:00
|
|
|
"""
|
2021-03-04 16:15:57 +01:00
|
|
|
if self.action in (
|
2019-06-03 17:04:30 +02:00
|
|
|
"partial_update",
|
|
|
|
"update",
|
|
|
|
"destroy",
|
|
|
|
"sort",
|
|
|
|
"assign",
|
|
|
|
"create",
|
|
|
|
):
|
2019-01-06 16:22:33 +01:00
|
|
|
result = (
|
|
|
|
has_perm(self.request.user, "agenda.can_see")
|
|
|
|
and has_perm(self.request.user, "agenda.can_see_internal_items")
|
|
|
|
and has_perm(self.request.user, "agenda.can_manage")
|
|
|
|
)
|
|
|
|
elif self.action in ("numbering",):
|
|
|
|
result = has_perm(self.request.user, "agenda.can_see") and has_perm(
|
|
|
|
self.request.user, "agenda.can_manage"
|
|
|
|
)
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
2015-01-06 00:14:49 +01:00
|
|
|
|
2019-06-03 17:04:30 +02:00
|
|
|
def create(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Creates an agenda item and adds the content object to the agenda.
|
|
|
|
Request args should specify the content object:
|
|
|
|
{
|
|
|
|
"collection": <The collection string>,
|
|
|
|
"id": <The content object id>
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
collection = request.data.get("collection")
|
|
|
|
id = request.data.get("id")
|
|
|
|
|
|
|
|
if not isinstance(collection, str):
|
|
|
|
raise ValidationError({"detail": "The collection needs to be a string"})
|
|
|
|
if not isinstance(id, int):
|
|
|
|
raise ValidationError({"detail": "The id needs to be an int"})
|
|
|
|
|
|
|
|
try:
|
|
|
|
model = get_model_from_collection_string(collection)
|
|
|
|
except ValueError:
|
2019-09-02 11:09:03 +02:00
|
|
|
raise ValidationError({"detail": "Invalid collection"})
|
2019-06-03 17:04:30 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
content_object = model.objects.get(pk=id)
|
|
|
|
except model.DoesNotExist:
|
|
|
|
raise ValidationError({"detail": "The id is invalid"})
|
|
|
|
|
|
|
|
if not hasattr(content_object, "get_agenda_title_information"):
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "The collection does not have agenda items"}
|
|
|
|
)
|
|
|
|
|
|
|
|
try:
|
|
|
|
item = Item.objects.create(content_object=content_object)
|
|
|
|
except IntegrityError:
|
|
|
|
raise ValidationError({"detail": "The item is already in the agenda"})
|
|
|
|
|
|
|
|
inform_changed_data(content_object)
|
|
|
|
return Response({id: item.id})
|
|
|
|
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Removes the item from the agenda. This does not delete the content
|
|
|
|
object. Also, the deletion is denied for items with topics as content objects.
|
|
|
|
"""
|
|
|
|
item = self.get_object()
|
|
|
|
content_object = item.content_object
|
|
|
|
if content_object.get_collection_string() == "topics/topic":
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "You cannot delete the agenda item to a topic"}
|
|
|
|
)
|
|
|
|
|
|
|
|
item.delete()
|
|
|
|
inform_changed_data(content_object)
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
2018-03-19 10:16:22 +01:00
|
|
|
def update(self, *args, **kwargs):
|
|
|
|
"""
|
2018-10-15 21:25:41 +02:00
|
|
|
Customized view endpoint to update all children if the item type has changed.
|
2019-05-13 15:50:27 +02:00
|
|
|
We do not check the level (affected by changing the parent) in fact that this
|
|
|
|
change is currentl only done via the sort view.
|
2018-03-19 10:16:22 +01:00
|
|
|
"""
|
2018-08-15 11:15:54 +02:00
|
|
|
old_type = self.get_object().type
|
2018-03-19 10:16:22 +01:00
|
|
|
|
2018-10-15 21:25:41 +02:00
|
|
|
response = super().update(*args, **kwargs)
|
2018-03-19 10:16:22 +01:00
|
|
|
|
2018-10-15 21:25:41 +02:00
|
|
|
# Update all children if the item type has changed.
|
2018-03-19 10:16:22 +01:00
|
|
|
item = self.get_object()
|
2018-08-15 11:15:54 +02:00
|
|
|
|
|
|
|
if old_type != item.type:
|
2018-03-19 10:16:22 +01:00
|
|
|
items_to_update = []
|
|
|
|
|
2018-10-15 21:25:41 +02:00
|
|
|
# Recursively add children to items_to_update.
|
2018-03-19 10:16:22 +01:00
|
|
|
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)
|
|
|
|
|
2018-10-15 21:25:41 +02:00
|
|
|
return response
|
2018-03-19 10:16:22 +01:00
|
|
|
|
2021-04-12 08:20:06 +02:00
|
|
|
@action(detail=False, methods=["post"])
|
2019-04-23 16:57:35 +02:00
|
|
|
def numbering(self, request):
|
|
|
|
"""
|
|
|
|
Auto numbering of the agenda according to the config. Manually added
|
|
|
|
item numbers will be overwritten.
|
|
|
|
"""
|
|
|
|
if not config["agenda_enable_numbering"]:
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "Numbering of agenda items is deactivated."}
|
|
|
|
)
|
|
|
|
|
|
|
|
Item.objects.number_all(numeral_system=config["agenda_numeral_system"])
|
|
|
|
return Response({"detail": "The agenda has been numbered."})
|
|
|
|
|
2021-04-12 08:20:06 +02:00
|
|
|
@action(detail=False, methods=["post"])
|
2019-04-23 16:57:35 +02:00
|
|
|
def sort(self, request):
|
|
|
|
"""
|
|
|
|
Sorts the whole agenda represented in a tree of ids. The request data should be a list (the root)
|
|
|
|
of all main agenda items. Each node is a dict with an id and optional children:
|
|
|
|
{
|
|
|
|
id: <the id>
|
|
|
|
children: [
|
|
|
|
<children, optional>
|
|
|
|
]
|
|
|
|
}
|
|
|
|
Every id has to be given.
|
|
|
|
"""
|
|
|
|
return self.sort_tree(request, Item, "weight", "parent_id")
|
|
|
|
|
2021-04-12 08:20:06 +02:00
|
|
|
@action(detail=False, methods=["post"])
|
2019-04-23 16:57:35 +02:00
|
|
|
@transaction.atomic
|
|
|
|
def assign(self, request):
|
|
|
|
"""
|
|
|
|
Assign multiple agenda items to a new parent item.
|
|
|
|
|
|
|
|
Send POST {... see schema ...} to assign the new parent.
|
|
|
|
|
|
|
|
This aslo checks the parent field to prevent hierarchical loops.
|
|
|
|
"""
|
|
|
|
schema = {
|
|
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
|
|
"title": "Agenda items assign new parent schema",
|
|
|
|
"description": "An object containing an array of agenda item ids and the new parent id the items should be assigned to.",
|
|
|
|
"type": "object",
|
|
|
|
"propterties": {
|
|
|
|
"items": {
|
|
|
|
"description": "An array of agenda item ids where the items should be assigned to the new parent id.",
|
|
|
|
"type": "array",
|
|
|
|
"items": {"type": "integer"},
|
|
|
|
"minItems": 1,
|
|
|
|
"uniqueItems": True,
|
|
|
|
},
|
|
|
|
"parent_id": {
|
|
|
|
"description": "The agenda item id of the new parent item.",
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"required": ["items", "parent_id"],
|
|
|
|
}
|
|
|
|
|
|
|
|
# Validate request data.
|
|
|
|
try:
|
|
|
|
jsonschema.validate(request.data, schema)
|
|
|
|
except jsonschema.ValidationError as err:
|
|
|
|
raise ValidationError({"detail": str(err)})
|
|
|
|
|
|
|
|
# Check parent item
|
|
|
|
try:
|
|
|
|
parent = Item.objects.get(pk=request.data["parent_id"])
|
|
|
|
except Item.DoesNotExist:
|
|
|
|
raise ValidationError(
|
2019-09-02 11:09:03 +02:00
|
|
|
{
|
|
|
|
"detail": "Parent item {0} does not exist",
|
|
|
|
"args": [request.data["parent_id"]],
|
|
|
|
}
|
2019-04-23 16:57:35 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
# Collect ancestors
|
|
|
|
ancestors = [parent.pk]
|
|
|
|
grandparent = parent.parent
|
|
|
|
while grandparent is not None:
|
|
|
|
ancestors.append(grandparent.pk)
|
|
|
|
grandparent = grandparent.parent
|
|
|
|
|
|
|
|
# First validate all items before changeing them.
|
|
|
|
items = []
|
|
|
|
for item_id in request.data["items"]:
|
|
|
|
# Prevent hierarchical loops.
|
|
|
|
if item_id in ancestors:
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-09-02 11:09:03 +02:00
|
|
|
"detail": "Assigning item {0} to one of its children is not possible.",
|
|
|
|
"args": [item_id],
|
2019-04-23 16:57:35 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
# Check every item
|
|
|
|
try:
|
|
|
|
items.append(Item.objects.get(pk=item_id))
|
|
|
|
except Item.DoesNotExist:
|
2019-09-02 11:09:03 +02:00
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "Item {0} does not exist", "args": [item_id]}
|
|
|
|
)
|
2019-04-23 16:57:35 +02:00
|
|
|
|
|
|
|
# OK, assign new parents.
|
|
|
|
for item in items:
|
|
|
|
# Assign new parent.
|
|
|
|
item.parent = parent
|
|
|
|
item.save(skip_autoupdate=True)
|
|
|
|
|
|
|
|
# Now inform all clients.
|
|
|
|
inform_changed_data(items)
|
|
|
|
|
|
|
|
# Send response.
|
2019-09-02 11:09:03 +02:00
|
|
|
return Response(
|
|
|
|
{"detail": "{0} items successfully assigned.", "args": [len(items)]}
|
|
|
|
)
|
2019-04-23 16:57:35 +02:00
|
|
|
|
|
|
|
|
2021-03-04 16:15:57 +01:00
|
|
|
class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
2019-04-23 16:57:35 +02:00
|
|
|
"""
|
|
|
|
API endpoint for agenda items.
|
|
|
|
|
|
|
|
There are some views, see check_view_permissions.
|
|
|
|
"""
|
|
|
|
|
|
|
|
queryset = ListOfSpeakers.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2021-03-04 16:15:57 +01:00
|
|
|
if self.action == "manage_speaker":
|
2019-04-23 16:57:35 +02:00
|
|
|
result = has_perm(self.request.user, "agenda.can_see_list_of_speakers")
|
|
|
|
# For manage_speaker requests the rest of the check is
|
|
|
|
# done in the specific method. See below.
|
2019-10-07 17:34:19 +02:00
|
|
|
elif self.action in (
|
|
|
|
"update",
|
|
|
|
"partial_update",
|
|
|
|
"speak",
|
|
|
|
"sort_speakers",
|
|
|
|
"readd_last_speaker",
|
2020-04-22 09:34:58 +02:00
|
|
|
"delete_all_speakers",
|
2019-10-07 17:34:19 +02:00
|
|
|
):
|
2019-04-23 16:57:35 +02:00
|
|
|
result = has_perm(
|
|
|
|
self.request.user, "agenda.can_see_list_of_speakers"
|
|
|
|
) and has_perm(self.request.user, "agenda.can_manage_list_of_speakers")
|
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
2021-04-12 08:20:06 +02:00
|
|
|
@action(detail=True, methods=["POST", "PATCH", "DELETE"])
|
2020-09-07 08:22:17 +02:00
|
|
|
@transaction.atomic
|
2015-05-26 18:21:30 +02:00
|
|
|
def manage_speaker(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Special view endpoint to add users to the list of speakers or remove
|
2020-10-14 15:08:14 +02:00
|
|
|
them. Send POST {'user': <user_id>} to add a new speaker.
|
2021-04-22 11:25:27 +02:00
|
|
|
Send POST {'user': <user_id>, 'point_of_order': True, 'note': <optional string> }
|
|
|
|
to add a pointof order to the list of speakers.
|
2020-10-14 15:08:14 +02:00
|
|
|
Omit data to add yourself. Send DELETE {'speaker': <speaker_id>} or
|
2016-07-07 13:33:18 +02:00
|
|
|
DELETE {'speaker': [<speaker_id>, <speaker_id>, ...]} to remove one or
|
|
|
|
more speakers from the list of speakers. Omit data to remove yourself.
|
2015-05-26 18:21:30 +02:00
|
|
|
|
|
|
|
Checks also whether the requesting user can do this. He needs at
|
2019-04-23 16:57:35 +02:00
|
|
|
least the permissions 'agenda.can_see_list_of_speakers' (see
|
2015-07-01 23:18:48 +02:00
|
|
|
self.check_view_permissions()). In case of adding himself the
|
|
|
|
permission 'agenda.can_be_speaker' is required. In case of adding
|
2019-04-23 16:57:35 +02:00
|
|
|
or removing someone else the permission 'agenda.can_manage_list_of_speakers'
|
|
|
|
is required. In case of removing himself no other permission is required.
|
2015-05-26 18:21:30 +02:00
|
|
|
"""
|
2019-04-23 16:57:35 +02:00
|
|
|
# Retrieve list of speakers.
|
|
|
|
list_of_speakers = self.get_object()
|
2015-05-26 18:21:30 +02:00
|
|
|
|
2019-04-23 16:57:35 +02:00
|
|
|
if request.method == "POST": # Add new speaker
|
2015-05-26 18:21:30 +02:00
|
|
|
# Retrieve user_id
|
2019-01-06 16:22:33 +01:00
|
|
|
user_id = request.data.get("user")
|
2020-10-14 15:08:14 +02:00
|
|
|
point_of_order = request.data.get("point_of_order") or False
|
|
|
|
if not isinstance(point_of_order, bool):
|
|
|
|
raise ValidationError({"detail": "point_of_order has to be a bool."})
|
2015-05-26 18:21:30 +02:00
|
|
|
|
2021-04-22 11:25:27 +02:00
|
|
|
note = request.data.get("note")
|
|
|
|
if note is not None and not isinstance(note, str):
|
|
|
|
raise ValidationError({"detail": "note must be a string"})
|
|
|
|
|
2015-05-26 18:21:30 +02:00
|
|
|
# Check permissions and other conditions. Get user instance.
|
|
|
|
if user_id is None:
|
|
|
|
# Add oneself
|
2021-03-01 14:31:40 +01:00
|
|
|
if not has_perm(self.request.user, "agenda.can_be_speaker"):
|
2015-05-26 18:21:30 +02:00
|
|
|
self.permission_denied(request)
|
2021-03-01 14:31:40 +01:00
|
|
|
|
2020-11-10 13:10:35 +01:00
|
|
|
# even if the list is closed, point of order has to be accepted
|
|
|
|
if not point_of_order and list_of_speakers.closed:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "The list of speakers is closed."})
|
2015-05-26 18:21:30 +02:00
|
|
|
user = self.request.user
|
|
|
|
else:
|
2020-11-09 08:54:41 +01:00
|
|
|
if not isinstance(user_id, int):
|
|
|
|
raise ValidationError({"detail": "user_id has to be an int."})
|
|
|
|
|
2020-10-14 15:08:14 +02:00
|
|
|
point_of_order = False # not for someone else
|
2015-05-26 18:21:30 +02:00
|
|
|
# Add someone else.
|
2019-01-06 16:22:33 +01:00
|
|
|
if not has_perm(
|
|
|
|
self.request.user, "agenda.can_manage_list_of_speakers"
|
|
|
|
):
|
2015-05-26 18:21:30 +02:00
|
|
|
self.permission_denied(request)
|
|
|
|
try:
|
2020-11-09 08:54:41 +01:00
|
|
|
user = get_user_model().objects.get(pk=user_id)
|
|
|
|
except get_user_model().DoesNotExist:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "User does not exist."})
|
2015-05-26 18:21:30 +02:00
|
|
|
|
|
|
|
# Try to add the user. This ensurse that a user is not twice in the
|
|
|
|
# list of coming speakers.
|
|
|
|
try:
|
2020-10-14 15:08:14 +02:00
|
|
|
speaker = Speaker.objects.add(
|
2021-04-22 11:25:27 +02:00
|
|
|
user, list_of_speakers, point_of_order=point_of_order, note=note
|
2020-10-14 15:08:14 +02:00
|
|
|
)
|
2015-05-26 18:21:30 +02:00
|
|
|
except OpenSlidesError as e:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": str(e)})
|
2015-05-26 18:21:30 +02:00
|
|
|
|
2017-04-28 22:10:18 +02:00
|
|
|
# Send new speaker via autoupdate because users without permission
|
|
|
|
# to see users may not have it but can get it now.
|
2019-11-05 11:26:43 +01:00
|
|
|
inform_changed_data(user, disable_history=True)
|
2017-04-28 22:10:18 +02:00
|
|
|
|
2021-04-22 11:25:27 +02:00
|
|
|
elif request.method == "DELETE":
|
2019-01-06 16:22:33 +01:00
|
|
|
speaker_ids = request.data.get("speaker")
|
2015-05-26 18:21:30 +02:00
|
|
|
|
|
|
|
# Check permissions and other conditions. Get speaker instance.
|
2016-07-07 13:33:18 +02:00
|
|
|
if speaker_ids is None:
|
2020-10-14 15:08:14 +02:00
|
|
|
point_of_order = request.data.get("point_of_order") or False
|
|
|
|
if not isinstance(point_of_order, bool):
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "point_of_order has to be a bool."}
|
|
|
|
)
|
2015-05-26 18:21:30 +02:00
|
|
|
# Remove oneself
|
|
|
|
queryset = Speaker.objects.filter(
|
2020-10-14 15:08:14 +02:00
|
|
|
list_of_speakers=list_of_speakers,
|
|
|
|
user=self.request.user,
|
|
|
|
point_of_order=point_of_order,
|
2019-01-06 16:22:33 +01:00
|
|
|
).exclude(weight=None)
|
2020-10-14 15:08:14 +02:00
|
|
|
|
|
|
|
if not queryset.exists():
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2020-10-14 15:08:14 +02:00
|
|
|
{"detail": "The user is not in the list of speakers."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2020-10-14 15:08:14 +02:00
|
|
|
# We delete all() from the queryset and do not use get():
|
|
|
|
# The Speaker.objects.add method should assert, that there
|
|
|
|
# is only one speaker. But due to race conditions, sometimes
|
|
|
|
# there are multiple ones. Using all() ensures, that there is
|
|
|
|
# no server crash, if this happens.
|
|
|
|
queryset.all().delete()
|
|
|
|
inform_changed_data(list_of_speakers)
|
2015-05-26 18:21:30 +02:00
|
|
|
else:
|
|
|
|
# Remove someone else.
|
2019-01-06 16:22:33 +01:00
|
|
|
if not has_perm(
|
|
|
|
self.request.user, "agenda.can_manage_list_of_speakers"
|
|
|
|
):
|
2015-05-26 18:21:30 +02:00
|
|
|
self.permission_denied(request)
|
2019-01-12 23:01:42 +01:00
|
|
|
if isinstance(speaker_ids, int):
|
2016-07-07 13:33:18 +02:00
|
|
|
speaker_ids = [speaker_ids]
|
2020-10-14 15:08:14 +02:00
|
|
|
deleted_some_speakers = False
|
2016-07-07 13:33:18 +02:00
|
|
|
for speaker_id in speaker_ids:
|
|
|
|
try:
|
|
|
|
speaker = Speaker.objects.get(pk=int(speaker_id))
|
|
|
|
except (ValueError, Speaker.DoesNotExist):
|
2017-01-15 10:49:48 +01:00
|
|
|
pass
|
|
|
|
else:
|
2017-03-21 11:06:05 +01:00
|
|
|
speaker.delete(skip_autoupdate=True)
|
2020-10-14 15:08:14 +02:00
|
|
|
deleted_some_speakers = True
|
2017-03-21 11:06:05 +01:00
|
|
|
# send autoupdate if speakers are deleted
|
2020-10-14 15:08:14 +02:00
|
|
|
if deleted_some_speakers:
|
2019-04-23 16:57:35 +02:00
|
|
|
inform_changed_data(list_of_speakers)
|
2021-04-22 11:25:27 +02:00
|
|
|
else:
|
|
|
|
raise ValidationError({"detail": "Invalid method"})
|
2017-03-21 11:06:05 +01:00
|
|
|
|
2019-01-12 23:01:42 +01:00
|
|
|
return Response()
|
2015-05-27 15:42:32 +02:00
|
|
|
|
2021-04-12 08:20:06 +02:00
|
|
|
@action(detail=True, methods=["PUT", "DELETE"])
|
2015-05-27 15:42:32 +02:00
|
|
|
def speak(self, request, pk=None):
|
|
|
|
"""
|
2015-09-04 18:24:41 +02:00
|
|
|
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.
|
2015-05-27 15:42:32 +02:00
|
|
|
"""
|
2019-04-23 16:57:35 +02:00
|
|
|
# Retrieve list_of_speakers.
|
|
|
|
list_of_speakers = self.get_object()
|
2015-05-27 15:42:32 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
if request.method == "PUT":
|
2015-05-27 15:42:32 +02:00
|
|
|
# Retrieve speaker_id
|
2019-01-06 16:22:33 +01:00
|
|
|
speaker_id = request.data.get("speaker")
|
2015-05-27 15:42:32 +02:00
|
|
|
if speaker_id is None:
|
2019-04-23 16:57:35 +02:00
|
|
|
speaker = list_of_speakers.get_next_speaker()
|
2015-05-27 15:42:32 +02:00
|
|
|
if speaker is None:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "The list of speakers is empty."})
|
2015-05-27 15:42:32 +02:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
speaker = Speaker.objects.get(pk=int(speaker_id))
|
|
|
|
except (ValueError, Speaker.DoesNotExist):
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "Speaker does not exist."})
|
2015-09-04 18:24:41 +02:00
|
|
|
speaker.begin_speech()
|
2019-01-12 23:01:42 +01:00
|
|
|
message = "User is now speaking."
|
2015-05-27 15:42:32 +02:00
|
|
|
|
|
|
|
else:
|
|
|
|
# request.method == 'DELETE'
|
|
|
|
try:
|
|
|
|
# We assume that there aren't multiple entries because this
|
2015-09-04 18:24:41 +02:00
|
|
|
# is forbidden by the Model's begin_speech method. We assume that
|
2015-05-27 15:42:32 +02:00
|
|
|
# there is only one speaker instance or none.
|
2019-01-06 16:22:33 +01:00
|
|
|
current_speaker = (
|
2019-04-23 16:57:35 +02:00
|
|
|
Speaker.objects.filter(
|
|
|
|
list_of_speakers=list_of_speakers, end_time=None
|
|
|
|
)
|
2019-01-06 16:22:33 +01:00
|
|
|
.exclude(begin_time=None)
|
|
|
|
.get()
|
|
|
|
)
|
2015-05-27 15:42:32 +02:00
|
|
|
except Speaker.DoesNotExist:
|
|
|
|
raise ValidationError(
|
2019-01-06 16:22:33 +01:00
|
|
|
{
|
2019-09-02 11:09:03 +02:00
|
|
|
"detail": "There is no one speaking at the moment according to {0}.",
|
|
|
|
"args": [list_of_speakers],
|
2019-01-06 16:22:33 +01:00
|
|
|
}
|
|
|
|
)
|
2015-09-04 18:24:41 +02:00
|
|
|
current_speaker.end_speech()
|
2019-01-12 23:01:42 +01:00
|
|
|
message = "The speech is finished now."
|
2015-05-26 18:21:30 +02:00
|
|
|
|
|
|
|
# Initiate response.
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"detail": message})
|
2015-05-26 18:21:30 +02:00
|
|
|
|
2021-04-12 08:20:06 +02:00
|
|
|
@action(detail=True, methods=["POST"])
|
2016-01-08 23:32:29 +01:00
|
|
|
def sort_speakers(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Special view endpoint to sort the list of speakers.
|
|
|
|
|
|
|
|
Expects a list of IDs of the speakers.
|
|
|
|
"""
|
2019-04-23 16:57:35 +02:00
|
|
|
# Retrieve list_of_speakers.
|
|
|
|
list_of_speakers = self.get_object()
|
2016-01-08 23:32:29 +01:00
|
|
|
|
|
|
|
# Check data
|
2019-01-06 16:22:33 +01:00
|
|
|
speaker_ids = request.data.get("speakers")
|
2016-01-08 23:32:29 +01:00
|
|
|
if not isinstance(speaker_ids, list):
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "Invalid data."})
|
2016-01-08 23:32:29 +01:00
|
|
|
|
|
|
|
# Get all speakers
|
|
|
|
speakers = {}
|
2019-04-23 16:57:35 +02:00
|
|
|
for speaker in list_of_speakers.speakers.filter(begin_time=None):
|
2016-01-08 23:32:29 +01:00
|
|
|
speakers[speaker.pk] = speaker
|
|
|
|
|
2015-12-19 00:29:20 +01:00
|
|
|
# Check and sort speakers
|
|
|
|
valid_speakers = []
|
2016-01-08 23:32:29 +01:00
|
|
|
for speaker_id in speaker_ids:
|
2015-12-19 00:29:20 +01:00
|
|
|
if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "Invalid data."})
|
2015-12-19 00:29:20 +01:00
|
|
|
valid_speakers.append(speakers[speaker_id])
|
2019-07-17 16:13:49 +02:00
|
|
|
weight = 1
|
2015-12-19 00:29:20 +01:00
|
|
|
with transaction.atomic():
|
|
|
|
for speaker in valid_speakers:
|
|
|
|
speaker.weight = weight
|
2016-12-06 12:21:29 +01:00
|
|
|
speaker.save(skip_autoupdate=True)
|
2015-12-19 00:29:20 +01:00
|
|
|
weight += 1
|
2016-01-08 23:32:29 +01:00
|
|
|
|
2016-12-06 12:21:29 +01:00
|
|
|
# send autoupdate
|
2019-04-23 16:57:35 +02:00
|
|
|
inform_changed_data(list_of_speakers)
|
2016-12-06 12:21:29 +01:00
|
|
|
|
2016-01-08 23:32:29 +01:00
|
|
|
# Initiate response.
|
2019-01-12 23:01:42 +01:00
|
|
|
return Response({"detail": "List of speakers successfully sorted."})
|
2019-10-07 17:34:19 +02:00
|
|
|
|
2021-04-12 08:20:06 +02:00
|
|
|
@action(detail=True, methods=["POST"])
|
2019-10-07 17:34:19 +02:00
|
|
|
def readd_last_speaker(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Special view endpoint to re-add the last finished speaker to the list of speakers.
|
|
|
|
"""
|
|
|
|
list_of_speakers = self.get_object()
|
|
|
|
|
|
|
|
# Retrieve speaker which spoke last and next speaker
|
2019-10-16 09:40:04 +02:00
|
|
|
last_speaker = (
|
|
|
|
list_of_speakers.speakers.exclude(end_time=None)
|
|
|
|
.order_by("-end_time")
|
|
|
|
.first()
|
|
|
|
)
|
|
|
|
if not last_speaker:
|
2019-10-07 17:34:19 +02:00
|
|
|
raise ValidationError({"detail": "There is no last speaker at the moment."})
|
|
|
|
|
2020-10-14 15:08:14 +02:00
|
|
|
if last_speaker.point_of_order:
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "You cannot readd a point of order speaker."}
|
|
|
|
)
|
|
|
|
|
|
|
|
if list_of_speakers.speakers.filter(
|
|
|
|
user=last_speaker.user, begin_time=None
|
|
|
|
).exists():
|
|
|
|
raise ValidationError({"detail": "The last speaker is already waiting."})
|
|
|
|
|
2019-10-07 17:34:19 +02:00
|
|
|
next_speaker = list_of_speakers.get_next_speaker()
|
|
|
|
new_weight = 1
|
|
|
|
# if there is a next speaker, insert last speaker before it
|
|
|
|
if next_speaker:
|
|
|
|
new_weight = next_speaker.weight - 1
|
|
|
|
|
|
|
|
# reset times of last speaker and prepend it to the list of active speakers
|
|
|
|
last_speaker.begin_time = last_speaker.end_time = None
|
|
|
|
last_speaker.weight = new_weight
|
|
|
|
last_speaker.save()
|
|
|
|
|
|
|
|
return Response()
|
2020-04-22 09:34:58 +02:00
|
|
|
|
2021-04-12 08:20:06 +02:00
|
|
|
@action(detail=False, methods=["post"])
|
2020-04-22 09:34:58 +02:00
|
|
|
def delete_all_speakers(self, request):
|
|
|
|
Speaker.objects.all().delete()
|
|
|
|
inform_changed_data(ListOfSpeakers.objects.all())
|
|
|
|
return Response()
|
2021-04-22 11:25:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
class SpeakerViewSet(UpdateModelMixin, GenericViewSet):
|
|
|
|
queryset = Speaker.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
|
|
|
return has_perm(self.request.user, "agenda.can_see_list_of_speakers")
|
|
|
|
|
|
|
|
def update(self, request, *args, **kwargs):
|
|
|
|
# This is a hack to make request.data mutable. Otherwise fields can not be deleted.
|
|
|
|
if isinstance(request.data, QueryDict):
|
|
|
|
request.data._mutable = True
|
|
|
|
|
|
|
|
if (
|
|
|
|
"pro_speech" in request.data
|
|
|
|
and not config["agenda_list_of_speakers_enable_pro_contra_speech"]
|
|
|
|
):
|
|
|
|
raise ValidationError({"detail": "pro/contra speech is not enabled"})
|
|
|
|
|
2021-04-29 07:53:02 +02:00
|
|
|
if "pro_speech" in request.data and "marked" in request.data:
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "pro_speech and marked cannot be given together"}
|
|
|
|
)
|
|
|
|
|
2021-04-22 11:25:27 +02:00
|
|
|
if not has_perm(request.user, "agenda.can_manage_list_of_speakers"):
|
|
|
|
# if no manage perms, only the speaker user itself can update the speaker.
|
|
|
|
speaker = self.get_object()
|
|
|
|
if speaker.user_id != request.user.id:
|
|
|
|
self.permission_denied(request)
|
|
|
|
|
|
|
|
whitelist = ["pro_speech"] # was checked above
|
|
|
|
if config["agenda_list_of_speakers_can_set_mark_self"]:
|
|
|
|
whitelist.append("marked")
|
|
|
|
|
|
|
|
for key in request.data.keys():
|
|
|
|
if key not in whitelist:
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": f"You are not allowed to set {key}"}
|
|
|
|
)
|
|
|
|
|
2021-04-29 07:53:02 +02:00
|
|
|
# toggle marked/pro_speech: If one is given, reset the other one
|
|
|
|
if request.data.get("pro_speech") in (True, False):
|
|
|
|
request.data["marked"] = False
|
|
|
|
if request.data.get("marked"):
|
|
|
|
request.data["pro_speech"] = None
|
|
|
|
|
2021-04-22 11:25:27 +02:00
|
|
|
return super().update(request, *args, **kwargs)
|