Skip to content
Snippets Groups Projects

Delete elements using list filters

Merged Erwan Rouchet requested to merge delete-list-elements into master
11 files
+ 526
47
Compare changes
  • Side-by-side
  • Inline
Files
11
@@ -10,7 +10,7 @@ from django.db.models.functions import Cast
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from psycopg2.extras import execute_values
from rest_framework import serializers, status
from rest_framework import permissions, serializers, status
from rest_framework.exceptions import NotFound, ValidationError
from rest_framework.generics import (
CreateAPIView,
@@ -21,6 +21,7 @@ from rest_framework.generics import (
RetrieveUpdateDestroyAPIView,
UpdateAPIView,
)
from rest_framework.mixins import DestroyModelMixin
from rest_framework.response import Response
from arkindex.dataimport.models import WorkerVersion
@@ -56,7 +57,7 @@ from arkindex.project.openapi import AutoSchema
from arkindex.project.pagination import PageNumberPagination
from arkindex.project.permissions import IsAuthenticated, IsVerified, IsVerifiedOrReadOnly
from arkindex.project.tools import BulkMap
from arkindex.project.triggers import corpus_delete
from arkindex.project.triggers import corpus_delete, element_trash
from arkindex_common.enums import TranscriptionType
classifications_queryset = Classification.objects.select_related('ml_class', 'source').order_by('-confidence')
@@ -124,6 +125,7 @@ class ElementsListMixin(object):
"""
serializer_class = ElementListSerializer
queryset = Element.objects.all()
permission_classes = (IsVerifiedOrReadOnly, )
openapi_overrides = {
'security': [],
'tags': ['elements'],
@@ -135,7 +137,7 @@ class ElementsListMixin(object):
'required': False,
'schema': {
'type': 'string',
}
},
},
{
'name': 'name',
@@ -153,7 +155,8 @@ class ElementsListMixin(object):
'schema': {'anyOf': [
{'type': 'string', 'format': 'uuid'},
{'type': 'boolean'},
]}
]},
'x-methods': ['get'],
},
{
'name': 'folder',
@@ -185,7 +188,8 @@ class ElementsListMixin(object):
'schema': {
'type': 'boolean',
'default': False,
}
},
'x-methods': ['get'],
},
{
'name': 'with_has_children',
@@ -198,7 +202,8 @@ class ElementsListMixin(object):
'schema': {
'type': 'boolean',
'default': False
}
},
'x-methods': ['get'],
},
{
'name': 'If-Modified-Since',
@@ -214,20 +219,66 @@ class ElementsListMixin(object):
'schema': {
# Cannot use format: date-time here as HTTP headers do not use ISO 8601
'type': 'string'
}
},
'x-methods': ['get'],
},
{
'name': 'delete_children',
'required': False,
'in': 'query',
'description': 'Delete all child elements of those elements recursively.',
'schema': {
'type': 'boolean',
'default': True
},
'x-methods': ['delete'],
}
]
}
def initial(self, *args, **kwargs):
"""
Adds a `self.clean_params` attribute that holds any query params or headers,
filtered by their allowed HTTP method.
Raise HTTP 400 early if any filter is not allowed in this method.
Does not perform validation on the filter values.
"""
super().initial(*args, **kwargs)
self.clean_params = {}
errors = {}
for param in self.openapi_overrides['parameters']:
if param['in'] == 'header':
origin_dict = self.request.headers
elif param['in'] == 'query':
origin_dict = self.request.query_params
else:
raise NotImplementedError
if param['name'] not in origin_dict:
continue
if param.get('x-methods') and self.request.method.lower() not in param['x-methods']:
errors[param['name']] = [f'This parameter is not allowed in {self.request.method} requests.']
else:
self.clean_params[param['name']] = origin_dict[param['name']]
if errors:
raise ValidationError(errors)
@cached_property
def selected_corpus(self):
corpus_id = self.kwargs.get('corpus')
if corpus_id is None:
return
# When a corpus is specified, check independently that it's readable
# by the current user. This prevents comparing the corpus of every element
return self.get_corpus(corpus_id)
# When a corpus is specified, check independently that it's readable/writable
# by the current user. This prevents comparing the corpus of every element.
# Require write rights for 'unsafe' methods (POST/PUT/PATCH/DELETE)
return self.get_corpus(
corpus_id,
right=Right.Read if self.request.method in permissions.SAFE_METHODS else Right.Write,
)
def get_filters(self):
filters = {}
@@ -237,16 +288,16 @@ class ElementsListMixin(object):
filters['corpus__in'] = Corpus.objects.readable(self.request.user)
if 'name' in self.request.query_params:
filters['name__icontains'] = self.request.query_params['name']
filters['name__icontains'] = self.clean_params['name']
if 'type' in self.request.query_params:
filters['type__slug'] = self.request.query_params['type']
filters['type__slug'] = self.clean_params['type']
only_folder = self.request.query_params.get('folder')
only_folder = self.clean_params.get('folder')
if only_folder is not None:
filters['type__folder'] = only_folder.lower() not in ('false', '0')
if 'worker_version' in self.request.query_params:
if 'worker_version' in self.clean_params:
try:
worker_version_id = uuid.UUID(self.request.query_params['worker_version'])
except (TypeError, ValueError):
@@ -267,7 +318,7 @@ class ElementsListMixin(object):
- elements with any best classes
- elements with a specific best class
"""
class_filter = self.request.query_params.get('best_class')
class_filter = self.clean_params.get('best_class')
if class_filter is None:
return
@@ -292,7 +343,7 @@ class ElementsListMixin(object):
def get_prefetch(self):
prefetch = {'corpus', 'zone__image__server', 'type'}
with_best_classes = self.request.query_params.get('with_best_classes')
with_best_classes = self.clean_params.get('with_best_classes')
if with_best_classes and with_best_classes.lower() not in ('false', '0'):
prefetch.add(best_classifications_prefetch)
@@ -313,7 +364,7 @@ class ElementsListMixin(object):
# Use queryset.distinct() whenever best_class is defined
queryset = queryset.filter(class_filters).distinct()
with_has_children = self.request.query_params.get('with_has_children')
with_has_children = self.clean_params.get('with_has_children')
if with_has_children and with_has_children.lower() not in ('false', '0'):
queryset = BulkMap(_fetch_has_children, queryset)
@@ -328,7 +379,7 @@ class ElementsListMixin(object):
queryset = queryset.iterable
assert isinstance(queryset, QuerySet), 'A Django QuerySet is required to check for modified elements'
modified_since_string = self.request.headers.get('If-Modified-Since')
modified_since_string = self.clean_params.get('If-Modified-Since')
if not modified_since_string:
return False
@@ -358,6 +409,16 @@ class ElementsListMixin(object):
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def delete(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
delete_children = self.clean_params.get('delete_children', '').lower() not in ('false', '0')
if not queryset.exists():
raise NotFound
element_trash(queryset, delete_children=delete_children)
return Response(status=status.HTTP_204_NO_CONTENT)
class DeprecatedElementsList(DeprecatedMixin, RetrieveAPIView):
"""
@@ -376,7 +437,7 @@ class DeprecatedElementsList(DeprecatedMixin, RetrieveAPIView):
)
class CorpusElements(ElementsListMixin, CorpusACLMixin, ListAPIView):
class CorpusElements(ElementsListMixin, CorpusACLMixin, DestroyModelMixin, ListAPIView):
"""
List elements in a corpus and filter by type, name, ML class
"""
@@ -396,7 +457,7 @@ class CorpusElements(ElementsListMixin, CorpusACLMixin, ListAPIView):
@property
def is_top_level(self):
return self.request.query_params.get('top_level') not in (None, 'false', '0')
return self.clean_params.get('top_level') not in (None, 'false', '0')
def get_queryset(self):
# Should not be possible due to the URL
@@ -421,7 +482,7 @@ class CorpusElements(ElementsListMixin, CorpusACLMixin, ListAPIView):
return super().get_order_by()
class ElementParents(ElementsListMixin, ListAPIView):
class ElementParents(ElementsListMixin, DestroyModelMixin, ListAPIView):
"""
List all parents of an element
"""
@@ -445,14 +506,22 @@ class ElementParents(ElementsListMixin, ListAPIView):
@property
def is_recursive(self):
recursive_param = self.request.query_params.get('recursive')
recursive_param = self.clean_params.get('recursive')
return recursive_param is not None and recursive_param.lower() not in ('false', '0')
def get_queryset(self):
if self.request.method in permissions.SAFE_METHODS:
corpora = Corpus.objects.readable(self.request.user)
else:
corpora = Corpus.objects.writable(self.request.user)
if not Element.objects.filter(id=self.kwargs['pk'], corpus__in=corpora).exists():
raise NotFound
return Element.objects.get_ascending(self.kwargs['pk'], recursive=self.is_recursive)
class ElementChildren(ElementsListMixin, ListAPIView):
class ElementChildren(ElementsListMixin, DestroyModelMixin, ListAPIView):
"""
List all children of an element
"""
@@ -476,7 +545,7 @@ class ElementChildren(ElementsListMixin, ListAPIView):
def get_filters(self):
filters = super().get_filters()
recursive_param = self.request.query_params.get('recursive')
recursive_param = self.clean_params.get('recursive')
if recursive_param is None or recursive_param.lower() in ('false', '0'):
# Only list direct children
@@ -492,6 +561,14 @@ class ElementChildren(ElementsListMixin, ListAPIView):
return ('paths__ordering', )
def get_queryset(self):
if self.request.method in permissions.SAFE_METHODS:
corpora = Corpus.objects.readable(self.request.user)
else:
corpora = Corpus.objects.writable(self.request.user)
if not Element.objects.filter(id=self.kwargs['pk'], corpus__in=corpora).exists():
raise NotFound
return Element.objects.get_descending(self.kwargs['pk'])
Loading