2016-12-03 21:59:13 +01:00
|
|
|
import re
|
2019-04-30 13:48:21 +02:00
|
|
|
from typing import List, Set
|
2016-09-13 11:54:30 +02:00
|
|
|
|
2018-11-24 23:14:41 +01:00
|
|
|
import jsonschema
|
2017-02-06 12:11:50 +01:00
|
|
|
from django.conf import settings
|
2018-06-12 14:17:02 +02:00
|
|
|
from django.contrib.auth import get_user_model
|
2017-06-18 20:20:44 +02:00
|
|
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
2016-07-29 23:33:47 +02:00
|
|
|
from django.db import IntegrityError, transaction
|
2018-06-26 15:59:05 +02:00
|
|
|
from django.db.models.deletion import ProtectedError
|
2018-06-12 14:17:02 +02:00
|
|
|
from django.http.request import QueryDict
|
2015-04-30 19:13:28 +02:00
|
|
|
from rest_framework import status
|
2013-04-24 15:07:39 +02:00
|
|
|
|
2019-03-06 14:53:24 +01:00
|
|
|
from ..core.config import config
|
|
|
|
from ..core.models import Tag
|
|
|
|
from ..utils.auth import has_perm, in_some_groups
|
|
|
|
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
|
|
|
from ..utils.rest_api import (
|
|
|
|
CreateModelMixin,
|
|
|
|
DestroyModelMixin,
|
|
|
|
GenericViewSet,
|
|
|
|
ModelViewSet,
|
|
|
|
Response,
|
|
|
|
ReturnDict,
|
|
|
|
UpdateModelMixin,
|
|
|
|
ValidationError,
|
|
|
|
detail_route,
|
|
|
|
list_route,
|
|
|
|
)
|
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 ..utils.views import TreeSortMixin
|
2016-02-11 22:58:32 +01:00
|
|
|
from .access_permissions import (
|
|
|
|
CategoryAccessPermissions,
|
|
|
|
MotionAccessPermissions,
|
2016-10-01 20:42:44 +02:00
|
|
|
MotionBlockAccessPermissions,
|
2016-09-10 18:49:38 +02:00
|
|
|
MotionChangeRecommendationAccessPermissions,
|
2018-08-31 15:33:41 +02:00
|
|
|
MotionCommentSectionAccessPermissions,
|
2018-09-24 10:28:31 +02:00
|
|
|
StatuteParagraphAccessPermissions,
|
2016-02-11 22:58:32 +01:00
|
|
|
WorkflowAccessPermissions,
|
|
|
|
)
|
2015-07-22 15:23:57 +02:00
|
|
|
from .exceptions import WorkflowError
|
2016-09-03 21:43:11 +02:00
|
|
|
from .models import (
|
|
|
|
Category,
|
|
|
|
Motion,
|
2016-10-01 20:42:44 +02:00
|
|
|
MotionBlock,
|
2016-09-10 18:49:38 +02:00
|
|
|
MotionChangeRecommendation,
|
2018-08-31 15:33:41 +02:00
|
|
|
MotionComment,
|
|
|
|
MotionCommentSection,
|
2016-09-03 21:43:11 +02:00
|
|
|
MotionPoll,
|
|
|
|
State,
|
2018-09-24 10:28:31 +02:00
|
|
|
StatuteParagraph,
|
2018-06-12 14:17:02 +02:00
|
|
|
Submitter,
|
2016-09-03 21:43:11 +02:00
|
|
|
Workflow,
|
|
|
|
)
|
2018-06-26 15:59:05 +02:00
|
|
|
from .serializers import MotionPollSerializer, StateSerializer
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-02-02 00:37:43 +01:00
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
# Viewsets for the REST API
|
|
|
|
|
2019-01-06 16:22:33 +01: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
|
|
|
class MotionViewSet(TreeSortMixin, ModelViewSet):
|
2015-01-24 16:35:50 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for motions.
|
|
|
|
|
2018-11-24 23:14:41 +01:00
|
|
|
There are a lot of views. See check_view_permissions().
|
2015-01-24 16:35:50 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = MotionAccessPermissions()
|
2015-01-24 16:35:50 +01:00
|
|
|
queryset = Motion.objects.all()
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
def check_view_permissions(self):
|
2015-01-24 16:35:50 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns True if the user has required permissions.
|
2015-01-24 16:35:50 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-17 22:26:23 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in ("metadata", "partial_update", "update", "destroy"):
|
|
|
|
result = has_perm(self.request.user, "motions.can_see")
|
2018-10-09 21:03:34 +02:00
|
|
|
# For partial_update, update and destroy requests the rest of the check is
|
2015-07-01 23:18:48 +02:00
|
|
|
# done in the update method. See below.
|
2019-03-19 21:26:28 +01:00
|
|
|
elif self.action in ("create", "set_state", "manage_comments"):
|
2019-01-18 19:41:05 +01:00
|
|
|
result = has_perm(self.request.user, "motions.can_see")
|
2019-03-19 21:26:28 +01:00
|
|
|
# The rest of the check is done in the respective method. See below.
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in (
|
2019-01-18 17:11:06 +01:00
|
|
|
"manage_multiple_state",
|
2019-01-06 16:22:33 +01:00
|
|
|
"set_recommendation",
|
|
|
|
"manage_multiple_recommendation",
|
|
|
|
"follow_recommendation",
|
|
|
|
"manage_multiple_submitters",
|
|
|
|
"manage_multiple_tags",
|
|
|
|
"create_poll",
|
|
|
|
):
|
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage_metadata"
|
|
|
|
)
|
2019-03-19 21:26:28 +01:00
|
|
|
elif self.action == "sort":
|
2019-01-06 16:22:33 +01:00
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage"
|
|
|
|
)
|
|
|
|
elif self.action == "support":
|
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_support"
|
|
|
|
)
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
2015-01-24 16:35:50 +01:00
|
|
|
|
2017-12-15 09:26:57 +01:00
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Destroy is allowed if the user has manage permissions, or he is the submitter and
|
|
|
|
the current state allows to edit the motion.
|
|
|
|
"""
|
|
|
|
motion = self.get_object()
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
if not (
|
|
|
|
(
|
|
|
|
has_perm(request.user, "motions.can_manage")
|
|
|
|
or motion.is_submitter(request.user)
|
|
|
|
and motion.state.allow_submitter_edit
|
|
|
|
)
|
|
|
|
):
|
2017-12-15 13:01:25 +01:00
|
|
|
self.permission_denied(request)
|
|
|
|
|
2018-11-04 14:02:30 +01:00
|
|
|
result = super().destroy(request, *args, **kwargs)
|
|
|
|
|
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_deleted_data(
|
|
|
|
[(motion.get_collection_string(), motion.pk)],
|
2019-01-19 15:49:46 +01:00
|
|
|
information=["Motion deleted"],
|
2019-01-06 16:22:33 +01:00
|
|
|
user_id=request.user.pk,
|
|
|
|
)
|
2018-11-04 14:02:30 +01:00
|
|
|
|
|
|
|
return result
|
2017-12-15 09:26:57 +01:00
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
def create(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Customized view endpoint to create a new motion.
|
|
|
|
"""
|
2018-07-09 23:22:26 +02:00
|
|
|
# This is a hack to make request.data mutable. Otherwise fields can not be deleted.
|
|
|
|
if isinstance(request.data, QueryDict):
|
|
|
|
request.data._mutable = True
|
|
|
|
|
2019-01-18 19:41:05 +01:00
|
|
|
# Check if amendment request and if parent motion exists. Check also permissions.
|
2019-01-06 16:22:33 +01:00
|
|
|
if request.data.get("parent_id") is not None:
|
2019-01-18 19:41:05 +01:00
|
|
|
# Amendment
|
|
|
|
if not has_perm(self.request.user, "motions.can_create_amendments"):
|
|
|
|
self.permission_denied(request)
|
2017-02-24 08:37:27 +01:00
|
|
|
try:
|
2019-01-06 16:22:33 +01:00
|
|
|
parent_motion = Motion.objects.get(pk=request.data["parent_id"])
|
2017-02-24 08:37:27 +01:00
|
|
|
except Motion.DoesNotExist:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "The parent motion does not exist."})
|
2017-02-27 15:37:01 +01:00
|
|
|
else:
|
2019-01-18 19:41:05 +01:00
|
|
|
# Common motion
|
|
|
|
if not has_perm(self.request.user, "motions.can_create"):
|
|
|
|
self.permission_denied(request)
|
2017-02-27 15:37:01 +01:00
|
|
|
parent_motion = None
|
2017-02-24 08:37:27 +01:00
|
|
|
|
2017-01-14 10:14:18 +01:00
|
|
|
# Check permission to send some data.
|
2019-01-06 16:22:33 +01:00
|
|
|
if not has_perm(request.user, "motions.can_manage"):
|
2017-03-28 18:41:08 +02:00
|
|
|
# Remove fields that the user is not allowed to send.
|
|
|
|
# The list() is required because we want to use del inside the loop.
|
|
|
|
keys = list(request.data.keys())
|
2019-02-15 11:54:20 +01:00
|
|
|
whitelist = [
|
|
|
|
"title",
|
|
|
|
"text",
|
|
|
|
"reason",
|
|
|
|
"category_id",
|
|
|
|
"statute_paragraph_id",
|
2019-02-19 12:13:45 +01:00
|
|
|
"workflow_id",
|
2019-02-15 11:54:20 +01:00
|
|
|
]
|
2017-02-27 15:37:01 +01:00
|
|
|
if parent_motion is not None:
|
|
|
|
# For creating amendments.
|
2019-01-06 16:22:33 +01:00
|
|
|
whitelist.extend(
|
|
|
|
[
|
|
|
|
"parent_id",
|
|
|
|
"amendment_paragraphs",
|
|
|
|
"motion_block_id", # This and the category_id will be set to the matching
|
|
|
|
# values from parent_motion.
|
|
|
|
]
|
|
|
|
)
|
|
|
|
request.data["category_id"] = parent_motion.category_id
|
|
|
|
request.data["motion_block_id"] = parent_motion.motion_block_id
|
2017-03-28 18:41:08 +02:00
|
|
|
for key in keys:
|
2017-01-14 10:14:18 +01:00
|
|
|
if key not in whitelist:
|
2017-03-28 18:41:08 +02:00
|
|
|
del request.data[key]
|
2016-10-01 20:42:44 +02:00
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
# Validate data and create motion.
|
2018-10-09 21:03:34 +02:00
|
|
|
# Attention: Even user without permission can_manage_metadata is allowed
|
|
|
|
# to create a new motion and set such metadata like category, motion block and origin.
|
2015-04-30 19:13:28 +02:00
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
motion = serializer.save(request_user=request.user)
|
|
|
|
|
2018-06-12 14:17:02 +02:00
|
|
|
# Check for submitters and make ids unique
|
|
|
|
if isinstance(request.data, QueryDict):
|
2019-01-06 16:22:33 +01:00
|
|
|
submitters_id = request.data.getlist("submitters_id")
|
2018-06-12 14:17:02 +02:00
|
|
|
else:
|
2019-01-06 16:22:33 +01:00
|
|
|
submitters_id = request.data.get("submitters_id")
|
2018-06-12 14:17:02 +02:00
|
|
|
if submitters_id is None:
|
|
|
|
submitters_id = []
|
|
|
|
if not isinstance(submitters_id, list):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "If submitters_id is given, it has to be a list."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-06-12 14:17:02 +02:00
|
|
|
|
|
|
|
submitters_id_unique = set()
|
|
|
|
for id in submitters_id:
|
|
|
|
try:
|
|
|
|
submitters_id_unique.add(int(id))
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
submitters = []
|
|
|
|
for submitter_id in submitters_id_unique:
|
|
|
|
try:
|
|
|
|
submitters.append(get_user_model().objects.get(pk=submitter_id))
|
|
|
|
except get_user_model().DoesNotExist:
|
|
|
|
continue # Do not add users that do not exist
|
|
|
|
|
|
|
|
# Add the request user, if he is authenticated and no submitters were given:
|
2019-01-12 23:01:42 +01:00
|
|
|
if not submitters and request.user.is_authenticated:
|
2018-06-12 14:17:02 +02:00
|
|
|
submitters.append(request.user)
|
|
|
|
|
|
|
|
# create all submitters
|
|
|
|
for submitter in submitters:
|
|
|
|
Submitter.objects.add(submitter, motion)
|
|
|
|
|
2017-04-28 22:10:18 +02:00
|
|
|
# Send new submitters and supporters via autoupdate because users
|
|
|
|
# without permission to see users may not have them but can get it now.
|
2019-01-19 15:49:46 +01:00
|
|
|
# TODO: Skip history.
|
2017-04-28 22:10:18 +02:00
|
|
|
new_users = list(motion.submitters.all())
|
|
|
|
new_users.extend(motion.supporters.all())
|
|
|
|
inform_changed_data(new_users)
|
|
|
|
|
2019-01-19 15:49:46 +01:00
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-20 10:05:50 +01:00
|
|
|
motion, information=["Motion created"], user_id=request.user.pk
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
headers = self.get_success_headers(serializer.data)
|
2018-10-15 21:25:41 +02:00
|
|
|
# Strip out response data so nobody gets unrestricted data.
|
2019-01-06 16:22:33 +01:00
|
|
|
data = ReturnDict(id=serializer.data.get("id"), serializer=serializer)
|
2018-10-15 21:25:41 +02:00
|
|
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
2015-04-30 19:13:28 +02:00
|
|
|
|
|
|
|
def update(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Customized view endpoint to update a motion.
|
|
|
|
|
|
|
|
Checks also whether the requesting user can update the motion. He
|
|
|
|
needs at least the permissions 'motions.can_see' (see
|
2016-12-09 18:00:45 +01:00
|
|
|
self.check_view_permissions()). Also check manage permission or
|
|
|
|
submitter and state.
|
2015-04-30 19:13:28 +02:00
|
|
|
"""
|
2018-07-09 23:22:26 +02:00
|
|
|
# This is a hack to make request.data mutable. Otherwise fields can not be deleted.
|
|
|
|
if isinstance(request.data, QueryDict):
|
|
|
|
request.data._mutable = True
|
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
# Get motion.
|
|
|
|
motion = self.get_object()
|
|
|
|
|
|
|
|
# Check permissions.
|
2019-01-06 16:22:33 +01:00
|
|
|
if (
|
|
|
|
not has_perm(request.user, "motions.can_manage")
|
|
|
|
and not has_perm(request.user, "motions.can_manage_metadata")
|
|
|
|
and not (
|
|
|
|
motion.is_submitter(request.user) and motion.state.allow_submitter_edit
|
|
|
|
)
|
|
|
|
):
|
2015-04-30 19:13:28 +02:00
|
|
|
self.permission_denied(request)
|
|
|
|
|
2016-01-14 23:44:19 +01:00
|
|
|
# Check permission to send only some data.
|
2018-10-29 15:08:09 +01:00
|
|
|
# Attention: Users with motions.can_manage permission can change all
|
|
|
|
# fields even if they do not have motions.can_manage_metadata
|
|
|
|
# permission.
|
2019-01-06 16:22:33 +01:00
|
|
|
if not has_perm(request.user, "motions.can_manage"):
|
2017-02-27 15:37:01 +01:00
|
|
|
# Remove fields that the user is not allowed to change.
|
|
|
|
# The list() is required because we want to use del inside the loop.
|
|
|
|
keys = list(request.data.keys())
|
2018-08-31 15:33:41 +02:00
|
|
|
whitelist: List[str] = []
|
2017-03-07 09:55:26 +01:00
|
|
|
# Add title, text and reason to the whitelist only, if the user is the submitter.
|
|
|
|
if motion.is_submitter(request.user) and motion.state.allow_submitter_edit:
|
2019-01-06 16:22:33 +01:00
|
|
|
whitelist.extend(("title", "text", "reason"))
|
|
|
|
|
|
|
|
if has_perm(request.user, "motions.can_manage_metadata"):
|
|
|
|
whitelist.extend(
|
|
|
|
("category_id", "motion_block_id", "origin", "supporters_id")
|
|
|
|
)
|
2018-10-09 21:03:34 +02:00
|
|
|
|
2016-01-14 23:44:19 +01:00
|
|
|
for key in keys:
|
|
|
|
if key not in whitelist:
|
|
|
|
del request.data[key]
|
2018-05-16 11:42:42 +02:00
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
# Validate data and update motion.
|
|
|
|
serializer = self.get_serializer(
|
2019-01-06 16:22:33 +01:00
|
|
|
motion, data=request.data, partial=kwargs.get("partial", False)
|
|
|
|
)
|
2015-04-30 19:13:28 +02:00
|
|
|
serializer.is_valid(raise_exception=True)
|
2018-08-31 15:33:41 +02:00
|
|
|
updated_motion = serializer.save()
|
2015-04-30 19:13:28 +02:00
|
|
|
|
2019-04-01 09:03:58 +02:00
|
|
|
# Check removal of supporters and initiate response.
|
2019-01-06 16:22:33 +01:00
|
|
|
if (
|
|
|
|
config["motions_remove_supporters"]
|
|
|
|
and updated_motion.state.allow_support
|
|
|
|
and not has_perm(request.user, "motions.can_manage")
|
|
|
|
):
|
2015-04-30 19:13:28 +02:00
|
|
|
updated_motion.supporters.clear()
|
2017-04-28 22:10:18 +02:00
|
|
|
|
2018-06-12 14:17:02 +02:00
|
|
|
# Send new supporters via autoupdate because users
|
2017-04-28 22:10:18 +02:00
|
|
|
# without permission to see users may not have them but can get it now.
|
2019-01-19 15:49:46 +01:00
|
|
|
# TODO: Skip history.
|
2018-06-12 14:17:02 +02:00
|
|
|
new_users = list(updated_motion.supporters.all())
|
2017-04-28 22:10:18 +02:00
|
|
|
inform_changed_data(new_users)
|
|
|
|
|
2018-11-04 14:02:30 +01:00
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-19 15:49:46 +01:00
|
|
|
updated_motion, information=["Motion updated"], user_id=request.user.pk
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-11-04 14:02:30 +01:00
|
|
|
|
2018-10-15 21:25:41 +02:00
|
|
|
# We do not add serializer.data to response so nobody gets unrestricted data here.
|
|
|
|
return Response()
|
2015-04-30 19:13:28 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@list_route(methods=["post"])
|
2018-09-24 10:28:31 +02:00
|
|
|
def sort(self, request):
|
|
|
|
"""
|
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
|
|
|
Sorts all motions 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.
|
2018-09-24 10:28:31 +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
|
|
|
return self.sort_tree(request, Motion, "weight", "sort_parent_id")
|
2018-09-24 10:28:31 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["POST", "DELETE"])
|
2018-08-31 15:33:41 +02:00
|
|
|
def manage_comments(self, request, pk=None):
|
2015-04-30 19:13:28 +02:00
|
|
|
"""
|
2018-10-16 12:41:46 +02:00
|
|
|
Create, update and delete motion comments.
|
2018-10-09 21:03:34 +02:00
|
|
|
|
|
|
|
Send a POST request with {'section_id': <id>, 'comment': '<comment>'}
|
|
|
|
to create a new comment or update an existing comment.
|
|
|
|
|
|
|
|
Send a DELETE request with just {'section_id': <id>} to delete the comment.
|
|
|
|
For every request, the user must have read and write permission for the given field.
|
2015-04-30 19:13:28 +02:00
|
|
|
"""
|
|
|
|
motion = self.get_object()
|
2018-08-31 15:33:41 +02:00
|
|
|
|
|
|
|
# Get the comment section
|
2019-01-06 16:22:33 +01:00
|
|
|
section_id = request.data.get("section_id")
|
2018-08-31 15:33:41 +02:00
|
|
|
if not section_id or not isinstance(section_id, int):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "You have to provide a section_id of type int."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-08-31 15:33:41 +02:00
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
try:
|
2018-08-31 15:33:41 +02:00
|
|
|
section = MotionCommentSection.objects.get(pk=section_id)
|
|
|
|
except MotionCommentSection.DoesNotExist:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": f"A comment section with id {section_id} does not exist."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-08-31 15:33:41 +02:00
|
|
|
|
|
|
|
# the request user needs to see and write to the comment section
|
2019-01-06 16:22:33 +01:00
|
|
|
if not in_some_groups(
|
|
|
|
request.user, list(section.read_groups.values_list("pk", flat=True))
|
|
|
|
) or not in_some_groups(
|
|
|
|
request.user, list(section.write_groups.values_list("pk", flat=True))
|
|
|
|
):
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-01-12 23:01:42 +01:00
|
|
|
"detail": "You are not allowed to see or write to the comment section."
|
2019-01-06 16:22:33 +01:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
if request.method == "POST": # Create or update
|
2018-08-31 15:33:41 +02:00
|
|
|
# validate comment
|
2019-01-06 16:22:33 +01:00
|
|
|
comment_value = request.data.get("comment", "")
|
2018-08-31 15:33:41 +02:00
|
|
|
if not isinstance(comment_value, str):
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "The comment should be a string."})
|
2018-08-31 15:33:41 +02:00
|
|
|
|
|
|
|
comment, created = MotionComment.objects.get_or_create(
|
2019-01-06 16:22:33 +01:00
|
|
|
motion=motion, section=section, defaults={"comment": comment_value}
|
|
|
|
)
|
2018-08-31 15:33:41 +02:00
|
|
|
if not created:
|
|
|
|
comment.comment = comment_value
|
|
|
|
comment.save()
|
|
|
|
|
2019-01-19 15:49:46 +01:00
|
|
|
message = ["Comment {arg1} updated", section.name]
|
2018-08-31 15:33:41 +02:00
|
|
|
else: # DELETE
|
|
|
|
try:
|
|
|
|
comment = MotionComment.objects.get(motion=motion, section=section)
|
|
|
|
except MotionComment.DoesNotExist:
|
2019-04-01 09:03:58 +02:00
|
|
|
# Be silent about not existing comments.
|
2018-08-31 15:33:41 +02:00
|
|
|
pass
|
|
|
|
else:
|
|
|
|
comment.delete()
|
2019-01-19 15:49:46 +01:00
|
|
|
message = ["Comment {arg1} deleted", section.name]
|
|
|
|
|
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-20 10:05:50 +01:00
|
|
|
motion, information=message, user_id=request.user.pk, restricted=True
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
2015-04-30 19:13:28 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"detail": message})
|
2015-04-30 19:13:28 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@list_route(methods=["post"])
|
2018-11-24 23:14:41 +01:00
|
|
|
@transaction.atomic
|
|
|
|
def manage_multiple_submitters(self, request):
|
|
|
|
"""
|
|
|
|
Set or reset submitters of multiple motions.
|
|
|
|
|
|
|
|
Send POST {"motions": [... see schema ...]} to changed the submitters.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
motions = request.data.get("motions")
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
schema = {
|
|
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
|
|
"title": "Motion manage multiple submitters schema",
|
|
|
|
"description": "An array of motion ids with the respective user ids that should be set as submitter.",
|
|
|
|
"type": "array",
|
|
|
|
"items": {
|
|
|
|
"type": "object",
|
|
|
|
"properties": {
|
2019-01-06 16:22:33 +01:00
|
|
|
"id": {"description": "The id of the motion.", "type": "integer"},
|
2018-11-24 23:14:41 +01:00
|
|
|
"submitters": {
|
|
|
|
"description": "An array of user ids the should become submitters. Use an empty array to clear submitter field.",
|
|
|
|
"type": "array",
|
2019-01-06 16:22:33 +01:00
|
|
|
"items": {"type": "integer"},
|
2018-11-24 23:14:41 +01:00
|
|
|
"uniqueItems": True,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"required": ["id", "submitters"],
|
|
|
|
},
|
|
|
|
"uniqueItems": True,
|
|
|
|
}
|
|
|
|
|
|
|
|
# Validate request data.
|
|
|
|
try:
|
|
|
|
jsonschema.validate(motions, schema)
|
|
|
|
except jsonschema.ValidationError as err:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": str(err)})
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
motion_result = []
|
|
|
|
new_submitters = []
|
|
|
|
for item in motions:
|
|
|
|
# Get motion.
|
|
|
|
try:
|
2019-01-06 16:22:33 +01:00
|
|
|
motion = Motion.objects.get(pk=item["id"])
|
2018-11-24 23:14:41 +01:00
|
|
|
except Motion.DoesNotExist:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": f"Motion {item['id']} does not exist"})
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
# Remove all submitters.
|
|
|
|
Submitter.objects.filter(motion=motion).delete()
|
|
|
|
|
|
|
|
# Set new submitters.
|
2019-01-06 16:22:33 +01:00
|
|
|
for submitter_id in item["submitters"]:
|
2018-11-24 23:14:41 +01:00
|
|
|
try:
|
|
|
|
submitter = get_user_model().objects.get(pk=submitter_id)
|
|
|
|
except get_user_model().DoesNotExist:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": f"Submitter {submitter_id} does not exist"}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-11-24 23:14:41 +01:00
|
|
|
Submitter.objects.add(submitter, motion)
|
|
|
|
new_submitters.append(submitter)
|
|
|
|
|
|
|
|
# Finish motion.
|
|
|
|
motion_result.append(motion)
|
|
|
|
|
|
|
|
# Now inform all clients.
|
2019-01-20 10:05:50 +01:00
|
|
|
inform_changed_data(
|
|
|
|
motion_result, information=["Submitters changed"], user_id=request.user.pk
|
|
|
|
)
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
# Also send all new submitters via autoupdate because users without
|
|
|
|
# permission to see users may not have them but can get it now.
|
|
|
|
# TODO: Skip history.
|
|
|
|
inform_changed_data(new_submitters)
|
|
|
|
|
|
|
|
# Send response.
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": f"{len(motion_result)} motions successfully updated."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-11-24 23:14:41 +01:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post", "delete"])
|
2015-04-30 19:13:28 +02:00
|
|
|
def support(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Special view endpoint to support a motion or withdraw support
|
|
|
|
(unsupport).
|
|
|
|
|
|
|
|
Send POST to support and DELETE to unsupport.
|
|
|
|
"""
|
|
|
|
# Retrieve motion and allowed actions.
|
|
|
|
motion = self.get_object()
|
|
|
|
|
|
|
|
# Support or unsupport motion.
|
2019-01-06 16:22:33 +01:00
|
|
|
if request.method == "POST":
|
2015-04-30 19:13:28 +02:00
|
|
|
# Support motion.
|
2019-01-06 16:22:33 +01:00
|
|
|
if not (
|
|
|
|
motion.state.allow_support
|
|
|
|
and config["motions_min_supporters"] > 0
|
|
|
|
and not motion.is_submitter(request.user)
|
|
|
|
and not motion.is_supporter(request.user)
|
|
|
|
):
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "You can not support this motion."})
|
2015-04-30 19:13:28 +02:00
|
|
|
motion.supporters.add(request.user)
|
2017-04-28 22:10:18 +02:00
|
|
|
# Send new supporter via autoupdate because users without permission
|
|
|
|
# to see users may not have it but can get it now.
|
2019-01-19 15:49:46 +01:00
|
|
|
# TODO: Skip history.
|
2017-04-28 22:10:18 +02:00
|
|
|
inform_changed_data([request.user])
|
2019-01-12 23:01:42 +01:00
|
|
|
message = "You have supported this motion successfully."
|
2015-04-30 19:13:28 +02:00
|
|
|
else:
|
|
|
|
# Unsupport motion.
|
|
|
|
# request.method == 'DELETE'
|
2016-12-09 18:00:45 +01:00
|
|
|
if not motion.state.allow_support or not motion.is_supporter(request.user):
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "You can not unsupport this motion."})
|
2015-04-30 19:13:28 +02:00
|
|
|
motion.supporters.remove(request.user)
|
2019-01-12 23:01:42 +01:00
|
|
|
message = "You have unsupported this motion successfully."
|
2015-04-30 19:13:28 +02:00
|
|
|
|
2019-01-19 15:49:46 +01:00
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
|
|
|
motion, information=["Supporters changed"], user_id=request.user.pk
|
|
|
|
)
|
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
# Initiate response.
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"detail": message})
|
2015-04-30 19:13:28 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["put"])
|
2015-04-30 19:13:28 +02:00
|
|
|
def set_state(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Special view endpoint to set and reset a state of a motion.
|
|
|
|
|
|
|
|
Send PUT {'state': <state_id>} to set and just PUT {} to reset the
|
|
|
|
state. Only managers can use this view.
|
2019-02-08 09:48:58 +01:00
|
|
|
|
|
|
|
If a state is given, it must be a next or previous state.
|
2015-04-30 19:13:28 +02:00
|
|
|
"""
|
|
|
|
# Retrieve motion and state.
|
|
|
|
motion = self.get_object()
|
2019-01-06 16:22:33 +01:00
|
|
|
state = request.data.get("state")
|
2015-04-30 19:13:28 +02:00
|
|
|
|
|
|
|
# Set or reset state.
|
|
|
|
if state is not None:
|
|
|
|
# Check data and set state.
|
2019-01-31 23:32:36 +01:00
|
|
|
if not has_perm(request.user, "motions.can_manage_metadata") and not (
|
|
|
|
motion.is_submitter(request.user) and motion.state.allow_submitter_edit
|
|
|
|
):
|
|
|
|
self.permission_denied(request)
|
2015-04-30 19:13:28 +02:00
|
|
|
try:
|
|
|
|
state_id = int(state)
|
|
|
|
except ValueError:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "Invalid data. State must be an integer."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2019-02-08 09:48:58 +01:00
|
|
|
if not motion.state.is_next_or_previous_state_id(state_id):
|
2015-04-30 19:13:28 +02:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": f"You can not set the state to {state_id}."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2015-04-30 19:13:28 +02:00
|
|
|
motion.set_state(state_id)
|
|
|
|
else:
|
|
|
|
# Reset state.
|
2019-01-31 23:32:36 +01:00
|
|
|
if not has_perm(self.request.user, "motions.can_manage_metadata"):
|
|
|
|
self.permission_denied(request)
|
2015-04-30 19:13:28 +02:00
|
|
|
motion.reset_state()
|
|
|
|
|
|
|
|
# Save motion.
|
2019-01-06 16:22:33 +01:00
|
|
|
motion.save(
|
2019-01-19 22:11:40 +01:00
|
|
|
update_fields=["state", "identifier", "identifier_number", "last_modified"],
|
2019-01-06 16:22:33 +01:00
|
|
|
skip_autoupdate=True,
|
|
|
|
)
|
2019-01-12 23:01:42 +01:00
|
|
|
message = f"The state of the motion was set to {motion.state.name}."
|
2015-04-30 19:13:28 +02:00
|
|
|
|
2019-04-26 13:03:10 +02:00
|
|
|
# Send submitters and supporters via autoupdate because users without
|
|
|
|
# users.can_see may see them now.
|
|
|
|
inform_changed_data(map(lambda s: s.user, motion.submitters.all()))
|
|
|
|
inform_changed_data(motion.supporters.all())
|
|
|
|
|
2019-01-19 15:49:46 +01:00
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
2019-01-20 10:05:50 +01:00
|
|
|
inform_changed_data(
|
|
|
|
motion,
|
|
|
|
information=["State set to {arg1}", motion.state.name],
|
|
|
|
user_id=request.user.pk,
|
|
|
|
)
|
2019-01-19 15:49:46 +01:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"detail": message})
|
2015-04-30 19:13:28 +02:00
|
|
|
|
2019-01-18 17:11:06 +01:00
|
|
|
@list_route(methods=["post"])
|
|
|
|
@transaction.atomic
|
|
|
|
def manage_multiple_state(self, request):
|
|
|
|
"""
|
|
|
|
Set or reset states of multiple motions.
|
|
|
|
|
|
|
|
Send POST {"motions": [... see schema ...]} to changed the states.
|
|
|
|
"""
|
|
|
|
motions = request.data.get("motions")
|
|
|
|
|
|
|
|
schema = {
|
|
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
|
|
"title": "Motion manage multiple state schema",
|
|
|
|
"description": "An array of motion ids with the respective state ids that should be set as new state.",
|
|
|
|
"type": "array",
|
|
|
|
"items": {
|
|
|
|
"type": "object",
|
|
|
|
"properties": {
|
|
|
|
"id": {"description": "The id of the motion.", "type": "integer"},
|
|
|
|
"state": {
|
|
|
|
"description": "The state id the should become the new state.",
|
|
|
|
"type": "integer",
|
|
|
|
"minimum": 1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"required": ["id", "state"],
|
|
|
|
},
|
|
|
|
"uniqueItems": True,
|
|
|
|
}
|
|
|
|
|
|
|
|
# Validate request data.
|
|
|
|
try:
|
|
|
|
jsonschema.validate(motions, schema)
|
|
|
|
except jsonschema.ValidationError as err:
|
|
|
|
raise ValidationError({"detail": str(err)})
|
|
|
|
|
|
|
|
motion_result = []
|
|
|
|
for item in motions:
|
|
|
|
# Get motion.
|
|
|
|
try:
|
|
|
|
motion = Motion.objects.get(pk=item["id"])
|
|
|
|
except Motion.DoesNotExist:
|
|
|
|
raise ValidationError({"detail": f"Motion {item['id']} does not exist"})
|
|
|
|
|
|
|
|
# Set or reset state.
|
|
|
|
state_id = item["state"]
|
|
|
|
valid_states = State.objects.filter(workflow=motion.workflow_id)
|
|
|
|
if state_id not in [item.id for item in valid_states]:
|
|
|
|
# States of different workflows are not allowed.
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": f"You can not set the state to {state_id}."}
|
|
|
|
)
|
|
|
|
motion.set_state(state_id)
|
|
|
|
|
|
|
|
# Save motion.
|
2019-03-01 21:11:14 +01:00
|
|
|
motion.save(
|
|
|
|
update_fields=[
|
|
|
|
"state",
|
|
|
|
"identifier",
|
|
|
|
"identifier_number",
|
|
|
|
"last_modified",
|
|
|
|
],
|
|
|
|
skip_autoupdate=True,
|
|
|
|
)
|
2019-01-18 17:11:06 +01:00
|
|
|
|
2019-04-26 13:03:10 +02:00
|
|
|
# Send submitters and supporters via autoupdate because users without
|
|
|
|
# users.can_see may see them now.
|
|
|
|
inform_changed_data(map(lambda s: s.user, motion.submitters.all()))
|
|
|
|
inform_changed_data(motion.supporters.all())
|
|
|
|
|
2019-01-19 15:49:46 +01:00
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-20 10:05:50 +01:00
|
|
|
motion,
|
|
|
|
information=["State set to {arg1}", motion.state.name],
|
|
|
|
user_id=request.user.pk,
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
|
|
|
|
2019-01-18 17:11:06 +01:00
|
|
|
# Finish motion.
|
|
|
|
motion_result.append(motion)
|
|
|
|
|
|
|
|
# Send response.
|
|
|
|
return Response(
|
2019-01-19 15:49:46 +01:00
|
|
|
{"detail": f"State of {len(motion_result)} motions successfully set."}
|
2019-01-18 17:11:06 +01:00
|
|
|
)
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["put"])
|
2016-09-03 21:43:11 +02:00
|
|
|
def set_recommendation(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Special view endpoint to set a recommendation of a motion.
|
|
|
|
|
|
|
|
Send PUT {'recommendation': <state_id>} to set and just PUT {} to
|
|
|
|
reset the recommendation. Only managers can use this view.
|
|
|
|
"""
|
|
|
|
# Retrieve motion and recommendation state.
|
|
|
|
motion = self.get_object()
|
2019-01-06 16:22:33 +01:00
|
|
|
recommendation_state = request.data.get("recommendation")
|
2016-09-03 21:43:11 +02:00
|
|
|
|
|
|
|
# Set or reset recommendation.
|
|
|
|
if recommendation_state is not None:
|
|
|
|
# Check data and set recommendation.
|
|
|
|
try:
|
|
|
|
recommendation_state_id = int(recommendation_state)
|
|
|
|
except ValueError:
|
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "Invalid data. Recommendation must be an integer."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
|
|
|
recommendable_states = State.objects.filter(
|
|
|
|
workflow=motion.workflow_id, recommendation_label__isnull=False
|
|
|
|
)
|
|
|
|
if recommendation_state_id not in [
|
|
|
|
item.id for item in recommendable_states
|
|
|
|
]:
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-01-12 23:01:42 +01:00
|
|
|
"detail": f"You can not set the recommendation to {recommendation_state_id}."
|
2019-01-06 16:22:33 +01:00
|
|
|
}
|
|
|
|
)
|
2016-09-03 21:43:11 +02:00
|
|
|
motion.set_recommendation(recommendation_state_id)
|
|
|
|
else:
|
|
|
|
# Reset recommendation.
|
|
|
|
motion.recommendation = None
|
|
|
|
|
|
|
|
# Save motion.
|
2019-01-20 10:05:50 +01:00
|
|
|
motion.save(
|
|
|
|
update_fields=["recommendation", "last_modified"], skip_autoupdate=True
|
|
|
|
)
|
2019-01-06 16:22:33 +01:00
|
|
|
label = (
|
|
|
|
motion.recommendation.recommendation_label
|
|
|
|
if motion.recommendation
|
|
|
|
else "None"
|
|
|
|
)
|
2019-01-12 23:01:42 +01:00
|
|
|
message = f"The recommendation of the motion was set to {label}."
|
2016-09-03 21:43:11 +02:00
|
|
|
|
2019-01-19 15:49:46 +01:00
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-20 10:05:50 +01:00
|
|
|
motion,
|
|
|
|
information=["Recommendation set to {arg1}", label],
|
|
|
|
user_id=request.user.pk,
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"detail": message})
|
2016-09-03 21:43:11 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@list_route(methods=["post"])
|
2018-11-24 23:14:41 +01:00
|
|
|
@transaction.atomic
|
|
|
|
def manage_multiple_recommendation(self, request):
|
|
|
|
"""
|
|
|
|
Set or reset recommendations of multiple motions.
|
|
|
|
|
|
|
|
Send POST {"motions": [... see schema ...]} to changed the recommendations.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
motions = request.data.get("motions")
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
schema = {
|
|
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
|
|
"title": "Motion manage multiple recommendations schema",
|
|
|
|
"description": "An array of motion ids with the respective state ids that should be set as recommendation.",
|
|
|
|
"type": "array",
|
|
|
|
"items": {
|
|
|
|
"type": "object",
|
|
|
|
"properties": {
|
2019-01-06 16:22:33 +01:00
|
|
|
"id": {"description": "The id of the motion.", "type": "integer"},
|
2018-11-24 23:14:41 +01:00
|
|
|
"recommendation": {
|
|
|
|
"description": "The state id the should become recommendation. Use 0 to clear recommendation field.",
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"required": ["id", "recommendation"],
|
|
|
|
},
|
|
|
|
"uniqueItems": True,
|
|
|
|
}
|
|
|
|
|
|
|
|
# Validate request data.
|
|
|
|
try:
|
|
|
|
jsonschema.validate(motions, schema)
|
|
|
|
except jsonschema.ValidationError as err:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": str(err)})
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
motion_result = []
|
|
|
|
for item in motions:
|
|
|
|
# Get motion.
|
|
|
|
try:
|
2019-01-06 16:22:33 +01:00
|
|
|
motion = Motion.objects.get(pk=item["id"])
|
2018-11-24 23:14:41 +01:00
|
|
|
except Motion.DoesNotExist:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": f"Motion {item['id']} does not exist"})
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
# Set or reset recommendation.
|
2019-01-06 16:22:33 +01:00
|
|
|
recommendation_state_id = item["recommendation"]
|
2018-11-24 23:14:41 +01:00
|
|
|
if recommendation_state_id == 0:
|
|
|
|
# Reset recommendation.
|
|
|
|
motion.recommendation = None
|
|
|
|
else:
|
|
|
|
# Check data and set recommendation.
|
2019-01-06 16:22:33 +01:00
|
|
|
recommendable_states = State.objects.filter(
|
|
|
|
workflow=motion.workflow_id, recommendation_label__isnull=False
|
|
|
|
)
|
|
|
|
if recommendation_state_id not in [
|
|
|
|
item.id for item in recommendable_states
|
|
|
|
]:
|
2018-11-24 23:14:41 +01:00
|
|
|
raise ValidationError(
|
2019-01-06 16:22:33 +01:00
|
|
|
{
|
2019-01-12 23:01:42 +01:00
|
|
|
"detail": "You can not set the recommendation to {recommendation_state_id}."
|
2019-01-06 16:22:33 +01:00
|
|
|
}
|
|
|
|
)
|
2018-11-24 23:14:41 +01:00
|
|
|
motion.set_recommendation(recommendation_state_id)
|
|
|
|
|
|
|
|
# Save motion.
|
2019-01-20 10:05:50 +01:00
|
|
|
motion.save(
|
|
|
|
update_fields=["recommendation", "last_modified"], skip_autoupdate=True
|
|
|
|
)
|
2019-01-06 16:22:33 +01:00
|
|
|
label = (
|
|
|
|
motion.recommendation.recommendation_label
|
|
|
|
if motion.recommendation
|
|
|
|
else "None"
|
|
|
|
)
|
2018-11-24 23:14:41 +01:00
|
|
|
|
2019-01-19 15:49:46 +01:00
|
|
|
# Fire autoupdate and save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-20 10:05:50 +01:00
|
|
|
motion,
|
|
|
|
information=["Recommendation set to {arg1}", label],
|
|
|
|
user_id=request.user.pk,
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
|
|
|
|
2018-11-24 23:14:41 +01:00
|
|
|
# Finish motion.
|
|
|
|
motion_result.append(motion)
|
|
|
|
|
|
|
|
# Send response.
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": f"{len(motion_result)} motions successfully updated."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-11-24 23:14:41 +01:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post"])
|
2017-11-11 12:28:24 +01:00
|
|
|
def follow_recommendation(self, request, pk=None):
|
|
|
|
motion = self.get_object()
|
|
|
|
if motion.recommendation is None:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "Cannot set an empty recommendation."})
|
2017-11-11 12:28:24 +01:00
|
|
|
|
2019-01-19 22:36:31 +01:00
|
|
|
motion.follow_recommendation()
|
2017-11-11 12:28:24 +01:00
|
|
|
|
|
|
|
motion.save(
|
2019-01-06 16:22:33 +01:00
|
|
|
update_fields=[
|
|
|
|
"state",
|
|
|
|
"identifier",
|
|
|
|
"identifier_number",
|
|
|
|
"state_extension",
|
2019-01-19 22:11:40 +01:00
|
|
|
"last_modified",
|
2019-01-06 16:22:33 +01:00
|
|
|
],
|
|
|
|
skip_autoupdate=True,
|
|
|
|
)
|
2017-11-11 12:28:24 +01:00
|
|
|
|
|
|
|
# Now send all changes to the clients.
|
2019-01-20 10:05:50 +01:00
|
|
|
inform_changed_data(
|
|
|
|
motion,
|
|
|
|
information=["State set to {arg1}", motion.state.name],
|
|
|
|
user_id=request.user.pk,
|
|
|
|
)
|
2019-01-19 15:49:46 +01:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"detail": "Recommendation followed successfully."})
|
2017-11-11 12:28:24 +01:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post"])
|
2015-07-22 15:23:57 +02:00
|
|
|
def create_poll(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
View to create a poll. It is a POST request without any data.
|
|
|
|
"""
|
|
|
|
motion = self.get_object()
|
2016-12-09 18:00:45 +01:00
|
|
|
if not motion.state.allow_create_poll:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "You can not create a poll in this motion state."}
|
|
|
|
)
|
2015-07-22 15:23:57 +02:00
|
|
|
try:
|
|
|
|
with transaction.atomic():
|
2018-03-26 14:01:52 +02:00
|
|
|
poll = motion.create_poll(skip_autoupdate=True)
|
2019-01-12 23:01:42 +01:00
|
|
|
except WorkflowError as err:
|
|
|
|
raise ValidationError({"detail": err})
|
2017-11-11 12:28:24 +01:00
|
|
|
|
2019-01-19 15:49:46 +01:00
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-20 10:05:50 +01:00
|
|
|
motion, information=["Vote created"], user_id=request.user.pk
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "Vote created successfully.", "createdPollId": poll.pk}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2015-07-22 15:23:57 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@list_route(methods=["post"])
|
2018-11-24 23:14:41 +01:00
|
|
|
@transaction.atomic
|
|
|
|
def manage_multiple_tags(self, request):
|
|
|
|
"""
|
|
|
|
Set or reset tags of multiple motions.
|
|
|
|
|
|
|
|
Send POST {"motions": [... see schema ...]} to changed the tags.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
motions = request.data.get("motions")
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
schema = {
|
|
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
|
|
"title": "Motion manage multiple tags schema",
|
|
|
|
"description": "An array of motion ids with the respective tags ids that should be set as tag.",
|
|
|
|
"type": "array",
|
|
|
|
"items": {
|
|
|
|
"type": "object",
|
|
|
|
"properties": {
|
2019-01-06 16:22:33 +01:00
|
|
|
"id": {"description": "The id of the motion.", "type": "integer"},
|
2018-11-24 23:14:41 +01:00
|
|
|
"tags": {
|
|
|
|
"description": "An array of tag ids the should become tags. Use an empty array to clear tag field.",
|
|
|
|
"type": "array",
|
2019-01-06 16:22:33 +01:00
|
|
|
"items": {"type": "integer"},
|
2018-11-24 23:14:41 +01:00
|
|
|
"uniqueItems": True,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"required": ["id", "tags"],
|
|
|
|
},
|
|
|
|
"uniqueItems": True,
|
|
|
|
}
|
|
|
|
|
|
|
|
# Validate request data.
|
|
|
|
try:
|
|
|
|
jsonschema.validate(motions, schema)
|
|
|
|
except jsonschema.ValidationError as err:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": str(err)})
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
motion_result = []
|
|
|
|
for item in motions:
|
|
|
|
# Get motion.
|
|
|
|
try:
|
2019-01-06 16:22:33 +01:00
|
|
|
motion = Motion.objects.get(pk=item["id"])
|
2018-11-24 23:14:41 +01:00
|
|
|
except Motion.DoesNotExist:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": f"Motion {item['id']} does not exist"})
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
# Set new tags
|
2019-01-06 16:22:33 +01:00
|
|
|
for tag_id in item["tags"]:
|
2018-11-24 23:14:41 +01:00
|
|
|
if not Tag.objects.filter(pk=tag_id).exists():
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": f"Tag {tag_id} does not exist"})
|
2019-01-06 16:22:33 +01:00
|
|
|
motion.tags.set(item["tags"])
|
2018-11-24 23:14:41 +01:00
|
|
|
|
|
|
|
# Finish motion.
|
|
|
|
motion_result.append(motion)
|
|
|
|
|
|
|
|
# Now inform all clients.
|
|
|
|
inform_changed_data(motion_result)
|
|
|
|
|
|
|
|
# Send response.
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": f"{len(motion_result)} motions successfully updated."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-11-24 23:14:41 +01:00
|
|
|
|
2015-07-22 15:23:57 +02:00
|
|
|
|
|
|
|
class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for motion polls.
|
|
|
|
|
2017-02-27 15:37:01 +01:00
|
|
|
There are the following views: update, partial_update and destroy.
|
2015-07-22 15:23:57 +02:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2015-07-22 15:23:57 +02:00
|
|
|
queryset = MotionPoll.objects.all()
|
|
|
|
serializer_class = MotionPollSerializer
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
return has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage_metadata"
|
|
|
|
)
|
2015-07-22 15:23:57 +02:00
|
|
|
|
2017-02-20 20:13:58 +01:00
|
|
|
def update(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Customized view endpoint to update a motion poll.
|
|
|
|
"""
|
2018-10-15 21:25:41 +02:00
|
|
|
response = super().update(*args, **kwargs)
|
2017-02-20 20:13:58 +01:00
|
|
|
poll = self.get_object()
|
2019-01-19 15:49:46 +01:00
|
|
|
|
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-21 15:07:55 +01:00
|
|
|
poll.motion, information=["Vote updated"], user_id=self.request.user.pk
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
|
|
|
|
2018-10-15 21:25:41 +02:00
|
|
|
return response
|
2017-02-20 20:13:58 +01:00
|
|
|
|
|
|
|
def destroy(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Customized view endpoint to delete a motion poll.
|
|
|
|
"""
|
|
|
|
poll = self.get_object()
|
2017-03-22 14:00:03 +01:00
|
|
|
result = super().destroy(*args, **kwargs)
|
2019-01-19 15:49:46 +01:00
|
|
|
|
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-21 15:07:55 +01:00
|
|
|
poll.motion, information=["Vote deleted"], user_id=self.request.user.pk
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
|
|
|
|
2017-02-20 20:13:58 +01:00
|
|
|
return result
|
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
|
2016-09-10 18:49:38 +02:00
|
|
|
class MotionChangeRecommendationViewSet(ModelViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for motion change recommendations.
|
|
|
|
|
|
|
|
There are the following views: metadata, list, retrieve, create,
|
|
|
|
partial_update, update and destroy.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-09-10 18:49:38 +02:00
|
|
|
access_permissions = MotionChangeRecommendationAccessPermissions()
|
|
|
|
queryset = MotionChangeRecommendation.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-10 18:49:38 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
|
|
|
result = has_perm(self.request.user, "motions.can_see")
|
|
|
|
elif self.action in ("create", "destroy", "partial_update", "update"):
|
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage"
|
|
|
|
)
|
2016-09-10 18:49:38 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
2017-06-18 20:20:44 +02:00
|
|
|
def create(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Creating a Change Recommendation, custom exception handling
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return super().create(request, *args, **kwargs)
|
|
|
|
except DjangoValidationError as err:
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"detail": err.message}, status=400)
|
2017-06-18 20:20:44 +02:00
|
|
|
|
2019-02-19 13:21:44 +01:00
|
|
|
def perform_create(self, serializer):
|
|
|
|
"""
|
|
|
|
Customized method to add history information.
|
|
|
|
"""
|
|
|
|
instance = serializer.save()
|
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
|
|
|
instance,
|
|
|
|
information=["Motion change recommendation created"],
|
|
|
|
user_id=self.request.user.pk,
|
|
|
|
)
|
|
|
|
|
|
|
|
def perform_update(self, serializer):
|
|
|
|
"""
|
|
|
|
Customized method to add history information.
|
|
|
|
"""
|
|
|
|
instance = serializer.save()
|
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
|
|
|
instance,
|
|
|
|
information=["Motion change recommendation updated"],
|
|
|
|
user_id=self.request.user.pk,
|
|
|
|
)
|
|
|
|
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Customized method to add history information.
|
|
|
|
"""
|
|
|
|
instance = self.get_object()
|
|
|
|
|
|
|
|
result = super().destroy(request, *args, **kwargs)
|
|
|
|
|
|
|
|
# Fire autoupdate again to save information to OpenSlides history.
|
|
|
|
inform_deleted_data(
|
|
|
|
[(instance.get_collection_string(), instance.pk)],
|
|
|
|
information=["Motion change recommendation deleted"],
|
|
|
|
user_id=request.user.pk,
|
|
|
|
)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
2016-09-10 18:49:38 +02:00
|
|
|
|
2018-08-31 15:33:41 +02:00
|
|
|
class MotionCommentSectionViewSet(ModelViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for motion comment fields.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2018-08-31 15:33:41 +02:00
|
|
|
access_permissions = MotionCommentSectionAccessPermissions()
|
|
|
|
queryset = MotionCommentSection.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2018-08-31 15:33:41 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in ("create", "destroy", "update", "partial_update"):
|
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage"
|
|
|
|
)
|
2018-08-31 15:33:41 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
|
|
|
def destroy(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Customized view endpoint to delete a motion comment section. Will return
|
|
|
|
an error for the user, if still comments for this section exist.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
result = super().destroy(*args, **kwargs)
|
2019-01-12 23:01:42 +01:00
|
|
|
except ProtectedError as err:
|
2018-08-31 15:33:41 +02:00
|
|
|
# The protected objects can just be motion comments.
|
2019-01-12 23:01:42 +01:00
|
|
|
motions = [f'"{comment.motion}"' for comment in err.protected_objects.all()]
|
2018-08-31 15:33:41 +02:00
|
|
|
count = len(motions)
|
2019-01-06 16:22:33 +01:00
|
|
|
motions_verbose = ", ".join(motions[:3])
|
2018-08-31 15:33:41 +02:00
|
|
|
if count > 3:
|
2019-01-06 16:22:33 +01:00
|
|
|
motions_verbose += ", ..."
|
2018-08-31 15:33:41 +02:00
|
|
|
|
|
|
|
if count == 1:
|
2019-01-12 23:01:42 +01:00
|
|
|
msg = f"This section has still comments in motion {motions_verbose}."
|
2018-08-31 15:33:41 +02:00
|
|
|
else:
|
2019-01-12 23:01:42 +01:00
|
|
|
msg = f"This section has still comments in motions {motions_verbose}."
|
2018-08-31 15:33:41 +02:00
|
|
|
|
2019-01-12 23:01:42 +01:00
|
|
|
msg += " " + "Please remove all comments before deletion."
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": msg})
|
2018-08-31 15:33:41 +02:00
|
|
|
return result
|
|
|
|
|
2019-04-15 10:53:52 +02:00
|
|
|
def update(self, *args, **kwargs):
|
|
|
|
response = super().update(*args, **kwargs)
|
|
|
|
# Update all affected motioncomments to update their `read_groups_id` field,
|
|
|
|
# which is taken from the updated section.
|
|
|
|
section = self.get_object()
|
|
|
|
inform_changed_data(MotionComment.objects.filter(section=section))
|
|
|
|
return response
|
|
|
|
|
2018-08-31 15:33:41 +02:00
|
|
|
|
2018-09-24 10:28:31 +02:00
|
|
|
class StatuteParagraphViewSet(ModelViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for statute paragraphs.
|
|
|
|
|
|
|
|
There are the following views: list, retrieve, create,
|
|
|
|
partial_update, update and destroy.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2018-09-24 10:28:31 +02:00
|
|
|
access_permissions = StatuteParagraphAccessPermissions()
|
|
|
|
queryset = StatuteParagraph.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2018-09-24 10:28:31 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage"
|
|
|
|
)
|
2018-09-24 10:28:31 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
class CategoryViewSet(ModelViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for categories.
|
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
There are the following views: metadata, list, retrieve, create,
|
2016-10-01 20:42:44 +02:00
|
|
|
partial_update, update, destroy and numbering.
|
2015-07-01 23:18:48 +02:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = CategoryAccessPermissions()
|
2015-07-01 23:18:48 +02:00
|
|
|
queryset = Category.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-17 22:26:23 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
|
|
|
result = has_perm(self.request.user, "motions.can_see")
|
|
|
|
elif self.action in (
|
|
|
|
"create",
|
|
|
|
"partial_update",
|
|
|
|
"update",
|
|
|
|
"destroy",
|
2019-04-30 13:48:21 +02:00
|
|
|
"sort",
|
2019-01-06 16:22:33 +01:00
|
|
|
"numbering",
|
|
|
|
):
|
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage"
|
|
|
|
)
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
2019-04-30 13:48:21 +02:00
|
|
|
@detail_route(methods=["post"])
|
|
|
|
@transaction.atomic
|
|
|
|
def sort(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Endpoint to sort all motions in the category.
|
|
|
|
|
|
|
|
Send POST {'motions': [<list of motion ids>]} to sort the given
|
|
|
|
motions in the given order. Ids of motions with another category or
|
|
|
|
non existing motions are ignored, but all motions of this category
|
|
|
|
have to be send.
|
|
|
|
"""
|
|
|
|
category = self.get_object()
|
|
|
|
|
|
|
|
ids = request.data.get("motions", None)
|
|
|
|
if not isinstance(ids, list):
|
|
|
|
raise ValidationError("The ids must be a list.")
|
|
|
|
|
|
|
|
motions = []
|
|
|
|
motion_ids: Set[int] = set() # To detect duplicated
|
|
|
|
for id in ids:
|
|
|
|
if not isinstance(id, int):
|
|
|
|
raise ValidationError("All ids must be int.")
|
|
|
|
|
|
|
|
if id in motion_ids:
|
|
|
|
continue # Duplicate id
|
|
|
|
|
|
|
|
try:
|
|
|
|
motion = Motion.objects.get(pk=id)
|
|
|
|
except Motion.DoesNotExist:
|
|
|
|
continue # Ignore invalid ids.
|
|
|
|
|
|
|
|
if motion.category is not None and motion.category.pk == category.pk:
|
|
|
|
motions.append(motion)
|
|
|
|
motion_ids.add(id)
|
|
|
|
|
|
|
|
if Motion.objects.filter(category=category).count() != len(motions):
|
|
|
|
raise ValidationError("Not all motions for this category are given")
|
|
|
|
|
|
|
|
# assign the category_weight field:
|
|
|
|
for weight, motion in enumerate(motions, start=1):
|
|
|
|
motion.category_weight = weight
|
|
|
|
motion.save(skip_autoupdate=True)
|
|
|
|
|
|
|
|
inform_changed_data(motions)
|
|
|
|
return Response()
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post"])
|
2016-07-13 01:39:28 +02:00
|
|
|
def numbering(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
Special view endpoint to number all motions in this category.
|
|
|
|
|
|
|
|
Only managers can use this view.
|
2016-08-15 23:53:21 +02:00
|
|
|
|
|
|
|
Send POST {'motions': [<list of motion ids>]} to sort the given
|
|
|
|
motions in a special order. Ids of motions which do not belong to
|
|
|
|
the category are just ignored. Send just POST {} to sort all
|
2016-12-03 21:59:13 +01:00
|
|
|
motions in the category by database id.
|
|
|
|
|
|
|
|
Amendments will get a new identifier prefix if the old prefix matches
|
|
|
|
the old parent motion identifier.
|
2016-07-13 01:39:28 +02:00
|
|
|
"""
|
|
|
|
category = self.get_object()
|
|
|
|
number = 0
|
2016-12-03 21:59:13 +01:00
|
|
|
instances = []
|
|
|
|
|
2017-02-06 12:11:50 +01:00
|
|
|
# If MOTION_IDENTIFIER_WITHOUT_BLANKS is set, don't use blanks when building identifier.
|
2019-01-06 16:22:33 +01:00
|
|
|
without_blank = (
|
|
|
|
hasattr(settings, "MOTION_IDENTIFIER_WITHOUT_BLANKS")
|
|
|
|
and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS
|
|
|
|
)
|
2017-02-06 12:11:50 +01:00
|
|
|
|
2016-12-03 21:59:13 +01:00
|
|
|
# Prepare ordered list of motions.
|
2016-07-13 01:39:28 +02:00
|
|
|
if not category.prefix:
|
2019-01-06 16:22:33 +01:00
|
|
|
prefix = ""
|
2017-02-06 12:11:50 +01:00
|
|
|
elif without_blank:
|
2019-01-12 23:01:42 +01:00
|
|
|
prefix = category.prefix
|
2016-07-13 01:39:28 +02:00
|
|
|
else:
|
2019-01-12 23:01:42 +01:00
|
|
|
prefix = f"{category.prefix} "
|
2016-08-15 23:53:21 +02:00
|
|
|
motions = category.motion_set.all()
|
2019-01-06 16:22:33 +01:00
|
|
|
motion_list = request.data.get("motions")
|
2016-08-15 23:53:21 +02:00
|
|
|
if motion_list:
|
|
|
|
motion_dict = {}
|
|
|
|
for motion in motions.filter(id__in=motion_list):
|
|
|
|
motion_dict[motion.pk] = motion
|
|
|
|
motions = [motion_dict[pk] for pk in motion_list]
|
2016-07-13 01:39:28 +02:00
|
|
|
|
2016-12-03 21:59:13 +01:00
|
|
|
# Change identifiers.
|
2018-05-22 07:44:21 +02:00
|
|
|
error_message = None
|
2016-07-29 23:33:47 +02:00
|
|
|
try:
|
|
|
|
with transaction.atomic():
|
2016-12-03 21:59:13 +01:00
|
|
|
# Collect old and new identifiers.
|
|
|
|
motions_to_be_sorted = []
|
2016-07-29 23:33:47 +02:00
|
|
|
for motion in motions:
|
|
|
|
if motion.is_amendment():
|
2019-01-06 16:22:33 +01:00
|
|
|
parent_identifier = motion.parent.identifier or ""
|
2017-02-06 12:11:50 +01:00
|
|
|
if without_blank:
|
2019-01-12 23:01:42 +01:00
|
|
|
prefix = f"{parent_identifier}{config['motions_amendments_prefix']}"
|
2017-02-06 12:11:50 +01:00
|
|
|
else:
|
2019-01-12 23:01:42 +01:00
|
|
|
prefix = f"{parent_identifier} {config['motions_amendments_prefix']} "
|
2016-07-29 23:33:47 +02:00
|
|
|
number += 1
|
2019-01-12 23:01:42 +01:00
|
|
|
new_identifier = (
|
|
|
|
f"{prefix}{motion.extend_identifier_number(number)}"
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
|
|
|
motions_to_be_sorted.append(
|
|
|
|
{
|
|
|
|
"motion": motion,
|
|
|
|
"old_identifier": motion.identifier,
|
|
|
|
"new_identifier": new_identifier,
|
|
|
|
"number": number,
|
|
|
|
}
|
|
|
|
)
|
2016-12-03 21:59:13 +01:00
|
|
|
|
|
|
|
# Remove old identifiers
|
|
|
|
for motion in motions:
|
|
|
|
motion.identifier = None
|
2018-01-20 09:54:46 +01:00
|
|
|
# This line is to skip agenda item autoupdate. See agenda/signals.py.
|
2019-01-06 16:22:33 +01:00
|
|
|
motion.agenda_item_update_information["skip_autoupdate"] = True
|
2016-12-03 21:59:13 +01:00
|
|
|
motion.save(skip_autoupdate=True)
|
|
|
|
|
|
|
|
# Set new identifers and change identifiers of amendments.
|
|
|
|
for obj in motions_to_be_sorted:
|
2019-01-06 16:22:33 +01:00
|
|
|
if Motion.objects.filter(identifier=obj["new_identifier"]).exists():
|
2018-05-22 07:44:21 +02:00
|
|
|
# Set the error message and let the code run into an IntegrityError
|
2019-01-12 23:01:42 +01:00
|
|
|
new_identifier = obj["new_identifier"]
|
2019-01-06 16:22:33 +01:00
|
|
|
error_message = (
|
2019-01-12 23:01:42 +01:00
|
|
|
f'Numbering aborted because the motion identifier "{new_identifier}" '
|
|
|
|
"already exists outside of this category."
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
|
|
|
motion = obj["motion"]
|
|
|
|
motion.identifier = obj["new_identifier"]
|
|
|
|
motion.identifier_number = obj["number"]
|
2016-12-02 15:47:57 +01:00
|
|
|
motion.save(skip_autoupdate=True)
|
|
|
|
instances.append(motion)
|
|
|
|
instances.append(motion.agenda_item)
|
2016-12-03 21:59:13 +01:00
|
|
|
# Change identifiers of amendments.
|
|
|
|
for child in motion.get_amendments_deep():
|
2019-01-06 16:22:33 +01:00
|
|
|
if child.identifier and child.identifier.startswith(
|
|
|
|
obj["old_identifier"]
|
|
|
|
):
|
2016-12-03 21:59:13 +01:00
|
|
|
child.identifier = re.sub(
|
2019-01-06 16:22:33 +01:00
|
|
|
obj["old_identifier"],
|
|
|
|
obj["new_identifier"],
|
2016-12-03 21:59:13 +01:00
|
|
|
child.identifier,
|
2019-01-06 16:22:33 +01:00
|
|
|
count=1,
|
|
|
|
)
|
2018-01-20 09:54:46 +01:00
|
|
|
# This line is to skip agenda item autoupdate. See agenda/signals.py.
|
2019-01-06 16:22:33 +01:00
|
|
|
child.agenda_item_update_information[
|
|
|
|
"skip_autoupdate"
|
|
|
|
] = True
|
2016-12-03 21:59:13 +01:00
|
|
|
child.save(skip_autoupdate=True)
|
|
|
|
instances.append(child)
|
|
|
|
instances.append(child.agenda_item)
|
2016-07-29 23:33:47 +02:00
|
|
|
except IntegrityError:
|
2018-05-22 07:44:21 +02:00
|
|
|
if error_message is None:
|
2019-01-12 23:01:42 +01:00
|
|
|
error_message = "Error: At least one identifier of this category does already exist in another category."
|
2019-01-06 16:22:33 +01:00
|
|
|
response = Response({"detail": error_message}, status=400)
|
2016-07-29 23:33:47 +02:00
|
|
|
else:
|
2019-01-20 10:05:50 +01:00
|
|
|
inform_changed_data(
|
|
|
|
instances, information=["Number set"], user_id=request.user.pk
|
|
|
|
)
|
2019-01-12 23:01:42 +01:00
|
|
|
message = f"All motions in category {category} numbered " "successfully."
|
2019-01-06 16:22:33 +01:00
|
|
|
response = Response({"detail": message})
|
2016-07-29 23:33:47 +02:00
|
|
|
return response
|
2016-07-13 01:39:28 +02:00
|
|
|
|
2016-10-01 20:42:44 +02:00
|
|
|
|
|
|
|
class MotionBlockViewSet(ModelViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for motion blocks.
|
|
|
|
|
|
|
|
There are the following views: metadata, list, retrieve, create,
|
|
|
|
partial_update, update and destroy.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-10-01 20:42:44 +02:00
|
|
|
access_permissions = MotionBlockAccessPermissions()
|
|
|
|
queryset = MotionBlock.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-10-01 20:42:44 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
|
|
|
result = has_perm(self.request.user, "motions.can_see")
|
|
|
|
elif self.action in (
|
|
|
|
"create",
|
|
|
|
"partial_update",
|
|
|
|
"update",
|
|
|
|
"destroy",
|
|
|
|
"follow_recommendations",
|
|
|
|
):
|
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage"
|
|
|
|
)
|
2016-10-01 20:42:44 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post"])
|
2016-10-14 21:48:02 +02:00
|
|
|
def follow_recommendations(self, request, pk=None):
|
|
|
|
"""
|
|
|
|
View to set the states of all motions of this motion block each to
|
|
|
|
its recommendation. It is a POST request without any data.
|
|
|
|
"""
|
|
|
|
motion_block = self.get_object()
|
|
|
|
with transaction.atomic():
|
|
|
|
for motion in motion_block.motion_set.all():
|
|
|
|
# Follow recommendation.
|
|
|
|
motion.follow_recommendation()
|
|
|
|
motion.save(skip_autoupdate=True)
|
2019-01-19 15:49:46 +01:00
|
|
|
# Fire autoupdate and save information to OpenSlides history.
|
|
|
|
inform_changed_data(
|
2019-01-20 10:05:50 +01:00
|
|
|
motion,
|
|
|
|
information=["State set to {arg1}", motion.state.name],
|
|
|
|
user_id=request.user.pk,
|
2019-01-19 15:49:46 +01:00
|
|
|
)
|
2019-01-12 23:01:42 +01:00
|
|
|
return Response({"detail": "Followed recommendations successfully."})
|
2016-10-14 21:48:02 +02:00
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
|
2018-06-26 15:59:05 +02:00
|
|
|
class ProtectedErrorMessageMixin:
|
|
|
|
def getProtectedErrorMessage(self, name, error):
|
|
|
|
# The protected objects can just be motions..
|
|
|
|
motions = ['"' + str(m) + '"' for m in error.protected_objects.all()]
|
|
|
|
count = len(motions)
|
2019-01-06 16:22:33 +01:00
|
|
|
motions_verbose = ", ".join(motions[:3])
|
2018-06-26 15:59:05 +02:00
|
|
|
if count > 3:
|
2019-01-06 16:22:33 +01:00
|
|
|
motions_verbose += ", ..."
|
2018-06-26 15:59:05 +02:00
|
|
|
|
|
|
|
if count == 1:
|
2019-01-12 23:01:42 +01:00
|
|
|
msg = f"This {name} is assigned to motion {motions_verbose}."
|
2018-06-26 15:59:05 +02:00
|
|
|
else:
|
2019-01-12 23:01:42 +01:00
|
|
|
msg = f"This {name} is assigned to motions {motions_verbose}."
|
|
|
|
return f"{msg} Please remove all assignments before deletion."
|
2018-06-26 15:59:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin):
|
2015-07-01 23:18:48 +02:00
|
|
|
"""
|
|
|
|
API endpoint for workflows.
|
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
There are the following views: metadata, list, retrieve, create,
|
|
|
|
partial_update, update and destroy.
|
2015-07-01 23:18:48 +02:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = WorkflowAccessPermissions()
|
2015-07-01 23:18:48 +02:00
|
|
|
queryset = Workflow.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-17 22:26:23 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
|
|
|
result = has_perm(self.request.user, "motions.can_see")
|
|
|
|
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
|
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage"
|
|
|
|
)
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
2018-06-26 15:59:05 +02:00
|
|
|
def destroy(self, *args, **kwargs):
|
|
|
|
"""
|
2018-08-31 15:33:41 +02:00
|
|
|
Customized view endpoint to delete a workflow.
|
2018-06-26 15:59:05 +02:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
result = super().destroy(*args, **kwargs)
|
2019-01-12 23:01:42 +01:00
|
|
|
except ProtectedError as err:
|
|
|
|
msg = self.getProtectedErrorMessage("workflow", err)
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": msg})
|
2018-06-26 15:59:05 +02:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
class StateViewSet(
|
|
|
|
CreateModelMixin,
|
|
|
|
UpdateModelMixin,
|
|
|
|
DestroyModelMixin,
|
|
|
|
GenericViewSet,
|
|
|
|
ProtectedErrorMessageMixin,
|
|
|
|
):
|
2018-06-26 15:59:05 +02:00
|
|
|
"""
|
|
|
|
API endpoint for workflow states.
|
|
|
|
|
|
|
|
There are the following views: create, update, partial_update and destroy.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2018-06-26 15:59:05 +02:00
|
|
|
queryset = State.objects.all()
|
|
|
|
serializer_class = StateSerializer
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
return has_perm(self.request.user, "motions.can_see") and has_perm(
|
|
|
|
self.request.user, "motions.can_manage"
|
|
|
|
)
|
2018-06-26 15:59:05 +02:00
|
|
|
|
|
|
|
def destroy(self, *args, **kwargs):
|
|
|
|
"""
|
2018-08-31 15:33:41 +02:00
|
|
|
Customized view endpoint to delete a state.
|
2018-06-26 15:59:05 +02:00
|
|
|
"""
|
|
|
|
state = self.get_object()
|
2019-01-19 14:02:13 +01:00
|
|
|
if state.workflow.first_state.pk == state.pk:
|
|
|
|
# is this the first state of the workflow?
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "You cannot delete the first state of the workflow."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-06-26 15:59:05 +02:00
|
|
|
try:
|
|
|
|
result = super().destroy(*args, **kwargs)
|
2019-01-12 23:01:42 +01:00
|
|
|
except ProtectedError as err:
|
|
|
|
msg = self.getProtectedErrorMessage("workflow", err)
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": msg})
|
2018-06-26 15:59:05 +02:00
|
|
|
return result
|
2019-04-16 15:45:59 +02:00
|
|
|
|
|
|
|
def update(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Sends autoupdate for all motions that are affected by the state change.
|
|
|
|
Maybe the restriction was changed, so the view permission for some
|
|
|
|
motions could have been changed.
|
|
|
|
"""
|
|
|
|
result = super().update(*args, **kwargs)
|
|
|
|
state = self.get_object()
|
|
|
|
inform_changed_data(Motion.objects.filter(state=state))
|
|
|
|
return result
|