OpenSlides/openslides/utils/views.py
FinnStutzenstein 10c329da8d fix tree sorting
Assigns the weight in the preorder traversal of the tree. Now one without every
object (e.g. missing motions/items) still have the correct sorting. Intorduces
the level attribute of items giving the amount of parents in the agenda. This
allows to reduce complexits in the client.
2019-05-15 14:14:32 +02:00

136 lines
4.8 KiB
Python

from typing import Any, Dict, List, Set
from django.db import models, transaction
from rest_framework.views import APIView as _APIView
from .autoupdate import inform_changed_data
from .rest_api import Response, ValidationError
class APIView(_APIView):
"""
The Django Rest framework APIView with improvements for OpenSlides.
"""
http_method_names: List[str] = []
"""
The allowed actions have to be explicitly defined.
Django allowes the following:
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
"""
def get_context_data(self, **context: Any) -> Dict[str, Any]:
"""
Returns the context for the response.
"""
return context
def method_call(self, request: Any, *args: Any, **kwargs: Any) -> Any:
"""
Http method that returns the response object with the context data.
"""
return Response(self.get_context_data())
# Add the http-methods and delete the method "method_call"
get = post = put = patch = delete = head = options = trace = method_call
del method_call
class TreeSortMixin:
"""
Provides a handler for sorting a model tree.
"""
def sort_tree(
self, request: Any, model: models.Model, weight_key: str, parent_id_key: str
) -> None:
"""
Sorts the all model objects represented in a tree of ids. The request data should
be a list (the root) of all main models. Each node is a dict with an id and optional children:
{
id: <the id>
children: [
<children, optional>
]
}
Every id has to be given.
This function traverses this tree in preorder to assign the weight. So even if a client
does not have every model, the remaining models are sorted correctly.
"""
if not isinstance(request.data, list):
raise ValidationError("The data must be a list.")
# get all item ids to verify, that the user send all ids.
all_model_ids = set(model.objects.all().values_list("pk", flat=True))
ids_found: Set[int] = set() # Set to save all found ids.
fake_root: Dict[str, Any] = {"id": None, "children": []}
fake_root["children"].extend(
request.data
) # this will prevent mutating the request data.
# The stack where all nodes to check are saved. Invariant: Each node
# must be a dict with an id, a parent id (may be None for the root
# layer) and a weight.
nodes_to_check = [fake_root]
# Traverse and check, if every id is given, valid and there are no duplicate ids.
weight = 1
while len(nodes_to_check) > 0:
node = nodes_to_check.pop()
id = node["id"]
if id is not None: # exclude the fake_root
node[weight_key] = weight
weight += 1
if id in ids_found:
raise ValidationError(f"Duplicate id: {id}")
if id not in all_model_ids:
raise ValidationError(f"Id does not exist: {id}")
ids_found.add(id)
# Add children, if exist.
if isinstance(node.get("children"), list):
node["children"].reverse()
for child in node["children"]:
# ensure invariant for nodes_to_check
if not isinstance(child, dict) or not isinstance(
child.get("id"), int
):
raise ValidationError(
"child must be a dict with an id as integer"
)
child[parent_id_key] = id
nodes_to_check.append(child)
if len(all_model_ids) != len(ids_found):
raise ValidationError(
f"Did not recieved {len(all_model_ids)} ids, got {len(ids_found)}."
)
# Do the actual update:
nodes_to_update = []
nodes_to_update.extend(
request.data
) # this will prevent mutating the request data.
with transaction.atomic():
while len(nodes_to_update) > 0:
node = nodes_to_update.pop()
id = node["id"]
weight = node[weight_key]
parent_id = node[parent_id_key]
db_node = model.objects.get(pk=id)
setattr(db_node, parent_id_key, parent_id)
setattr(db_node, weight_key, weight)
db_node.save(skip_autoupdate=True)
# Add children, if exist.
children = node.get("children")
if isinstance(children, list):
nodes_to_update.extend(children)
inform_changed_data(model.objects.all())
return Response()