You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

452 lines
17 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
5 years ago
  1. import collections
  2. import json
  3. import random
  4. from django.conf import settings
  5. from django.contrib.auth.models import User
  6. from django.db import transaction
  7. from django.db.utils import IntegrityError
  8. from django.shortcuts import get_object_or_404, redirect
  9. from django_filters.rest_framework import DjangoFilterBackend
  10. from django.db.models import Count, F, Q
  11. from libcloud.base import DriverType, get_driver
  12. from libcloud.storage.types import ContainerDoesNotExistError, ObjectDoesNotExistError
  13. from rest_framework import generics, filters, status
  14. from rest_framework.exceptions import ParseError, ValidationError
  15. from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
  16. from rest_framework.response import Response
  17. from rest_framework.views import APIView
  18. from rest_framework.parsers import MultiPartParser
  19. from rest_framework_csv.renderers import CSVRenderer
  20. from .filters import DocumentFilter
  21. from .models import Project, Label, Document, RoleMapping, Role
  22. from .permissions import IsProjectAdmin, IsAnnotatorAndReadOnly, IsAnnotator, IsAnnotationApproverAndReadOnly, IsOwnAnnotation, IsAnnotationApprover
  23. from .serializers import ProjectSerializer, LabelSerializer, DocumentSerializer, UserSerializer, ApproverSerializer
  24. from .serializers import ProjectPolymorphicSerializer, RoleMappingSerializer, RoleSerializer
  25. from .utils import CSVParser, ExcelParser, JSONParser, PlainTextParser, CoNLLParser, AudioParser, iterable_to_io
  26. from .utils import JSONLRenderer
  27. from .utils import JSONPainter, CSVPainter
  28. IsInProjectReadOnlyOrAdmin = (IsAnnotatorAndReadOnly | IsAnnotationApproverAndReadOnly | IsProjectAdmin)
  29. IsInProjectOrAdmin = (IsAnnotator | IsAnnotationApprover | IsProjectAdmin)
  30. class Health(APIView):
  31. permission_classes = (IsAuthenticatedOrReadOnly,)
  32. def get(self, request, *args, **kwargs):
  33. return Response({'status': 'green'})
  34. class Me(APIView):
  35. permission_classes = (IsAuthenticated,)
  36. def get(self, request, *args, **kwargs):
  37. serializer = UserSerializer(request.user, context={'request': request})
  38. return Response(serializer.data)
  39. class Features(APIView):
  40. permission_classes = (IsAuthenticated,)
  41. def get(self, request, *args, **kwargs):
  42. return Response({
  43. 'cloud_upload': bool(settings.CLOUD_BROWSER_APACHE_LIBCLOUD_PROVIDER),
  44. })
  45. class ProjectList(generics.ListCreateAPIView):
  46. serializer_class = ProjectPolymorphicSerializer
  47. pagination_class = None
  48. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  49. def get_queryset(self):
  50. return self.request.user.projects
  51. def perform_create(self, serializer):
  52. serializer.save(users=[self.request.user])
  53. class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
  54. queryset = Project.objects.all()
  55. serializer_class = ProjectSerializer
  56. lookup_url_kwarg = 'project_id'
  57. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  58. class StatisticsAPI(APIView):
  59. pagination_class = None
  60. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  61. def get(self, request, *args, **kwargs):
  62. p = get_object_or_404(Project, pk=self.kwargs['project_id'])
  63. include = set(request.GET.getlist('include'))
  64. response = {}
  65. if not include or 'label' in include:
  66. label_count, user_count = self.label_per_data(p)
  67. response['label'] = label_count
  68. # TODO: Make user_label count chart
  69. response['user_label'] = user_count
  70. if not include or 'total' in include or 'remaining' in include or 'user' in include:
  71. progress = self.progress(project=p)
  72. response.update(progress)
  73. if include:
  74. response = {key: value for (key, value) in response.items() if key in include}
  75. return Response(response)
  76. @staticmethod
  77. def _get_user_completion_data(annotation_class, annotation_filter):
  78. all_annotation_objects = annotation_class.objects.filter(annotation_filter)
  79. set_user_data = collections.defaultdict(set)
  80. for ind_obj in all_annotation_objects.values('user__username', 'document__id'):
  81. set_user_data[ind_obj['user__username']].add(ind_obj['document__id'])
  82. return {i: len(set_user_data[i]) for i in set_user_data}
  83. def progress(self, project):
  84. docs = project.documents
  85. annotation_class = project.get_annotation_class()
  86. total = docs.count()
  87. annotation_filter = Q(document_id__in=docs.all())
  88. user_data = self._get_user_completion_data(annotation_class, annotation_filter)
  89. if not project.collaborative_annotation:
  90. annotation_filter &= Q(user_id=self.request.user)
  91. done = annotation_class.objects.filter(annotation_filter)\
  92. .aggregate(Count('document', distinct=True))['document__count']
  93. remaining = total - done
  94. return {'total': total, 'remaining': remaining, 'user': user_data}
  95. def label_per_data(self, project):
  96. annotation_class = project.get_annotation_class()
  97. return annotation_class.objects.get_label_per_data(project=project)
  98. class ApproveLabelsAPI(APIView):
  99. permission_classes = [IsAuthenticated & (IsAnnotationApprover | IsProjectAdmin)]
  100. def post(self, request, *args, **kwargs):
  101. approved = self.request.data.get('approved', True)
  102. document = get_object_or_404(Document, pk=self.kwargs['doc_id'])
  103. document.annotations_approved_by = self.request.user if approved else None
  104. document.save()
  105. return Response(ApproverSerializer(document).data)
  106. class LabelList(generics.ListCreateAPIView):
  107. serializer_class = LabelSerializer
  108. pagination_class = None
  109. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  110. def get_queryset(self):
  111. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  112. return project.labels
  113. def perform_create(self, serializer):
  114. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  115. serializer.save(project=project)
  116. class LabelDetail(generics.RetrieveUpdateDestroyAPIView):
  117. queryset = Label.objects.all()
  118. serializer_class = LabelSerializer
  119. lookup_url_kwarg = 'label_id'
  120. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  121. class DocumentList(generics.ListCreateAPIView):
  122. serializer_class = DocumentSerializer
  123. filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
  124. search_fields = ('text', )
  125. ordering_fields = ('created_at', 'updated_at', 'doc_annotations__updated_at',
  126. 'seq_annotations__updated_at', 'seq2seq_annotations__updated_at')
  127. filter_class = DocumentFilter
  128. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  129. def get_queryset(self):
  130. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  131. queryset = project.documents
  132. if project.randomize_document_order:
  133. random.seed(self.request.user.id)
  134. value = random.randrange(2, 20)
  135. queryset = queryset.annotate(sort_id=F('id') % value).order_by('sort_id', 'id')
  136. else:
  137. queryset = queryset.order_by('id')
  138. return queryset
  139. def perform_create(self, serializer):
  140. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  141. serializer.save(project=project)
  142. def delete(self, request, *args, **kwargs):
  143. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  144. queryset = project.documents
  145. queryset.all().delete()
  146. return Response(status=status.HTTP_204_NO_CONTENT)
  147. class DocumentDetail(generics.RetrieveUpdateDestroyAPIView):
  148. queryset = Document.objects.all()
  149. serializer_class = DocumentSerializer
  150. lookup_url_kwarg = 'doc_id'
  151. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  152. class AnnotationList(generics.ListCreateAPIView):
  153. pagination_class = None
  154. permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
  155. swagger_schema = None
  156. def get_serializer_class(self):
  157. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  158. self.serializer_class = project.get_annotation_serializer()
  159. return self.serializer_class
  160. def get_queryset(self):
  161. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  162. model = project.get_annotation_class()
  163. queryset = model.objects.filter(document=self.kwargs['doc_id'])
  164. if not project.collaborative_annotation:
  165. queryset = queryset.filter(user=self.request.user)
  166. return queryset
  167. def create(self, request, *args, **kwargs):
  168. self.check_single_class_classification(self.kwargs['project_id'], self.kwargs['doc_id'], request.user)
  169. request.data['document'] = self.kwargs['doc_id']
  170. return super().create(request, args, kwargs)
  171. def perform_create(self, serializer):
  172. serializer.save(document_id=self.kwargs['doc_id'], user=self.request.user)
  173. def delete(self, request, *args, **kwargs):
  174. queryset = self.get_queryset()
  175. queryset.all().delete()
  176. return Response(status=status.HTTP_204_NO_CONTENT)
  177. @staticmethod
  178. def check_single_class_classification(project_id, doc_id, user):
  179. project = get_object_or_404(Project, pk=project_id)
  180. if not project.single_class_classification:
  181. return
  182. model = project.get_annotation_class()
  183. annotations = model.objects.filter(document_id=doc_id)
  184. if not project.collaborative_annotation:
  185. annotations = annotations.filter(user=user)
  186. if annotations.exists():
  187. raise ValidationError('requested to create duplicate annotation for single-class-classification project')
  188. class AnnotationDetail(generics.RetrieveUpdateDestroyAPIView):
  189. lookup_url_kwarg = 'annotation_id'
  190. swagger_schema = None
  191. def get_permissions(self):
  192. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  193. if project.collaborative_annotation:
  194. self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
  195. else:
  196. self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin & IsOwnAnnotation]
  197. return super().get_permissions()
  198. def get_serializer_class(self):
  199. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  200. self.serializer_class = project.get_annotation_serializer()
  201. return self.serializer_class
  202. def get_queryset(self):
  203. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  204. model = project.get_annotation_class()
  205. self.queryset = model.objects.all()
  206. return self.queryset
  207. class TextUploadAPI(APIView):
  208. parser_classes = (MultiPartParser,)
  209. permission_classes = [IsAuthenticated & IsProjectAdmin]
  210. def post(self, request, *args, **kwargs):
  211. if 'file' not in request.data:
  212. raise ParseError('Empty content')
  213. self.save_file(
  214. user=request.user,
  215. file=request.data['file'],
  216. file_format=request.data['format'],
  217. project_id=kwargs['project_id'],
  218. )
  219. return Response(status=status.HTTP_201_CREATED)
  220. @classmethod
  221. def save_file(cls, user, file, file_format, project_id):
  222. project = get_object_or_404(Project, pk=project_id)
  223. parser = cls.select_parser(file_format)
  224. data = parser.parse(file)
  225. storage = project.get_storage(data)
  226. storage.save(user)
  227. @classmethod
  228. def select_parser(cls, file_format):
  229. if file_format == 'plain':
  230. return PlainTextParser()
  231. elif file_format == 'csv':
  232. return CSVParser()
  233. elif file_format == 'json':
  234. return JSONParser()
  235. elif file_format == 'conll':
  236. return CoNLLParser()
  237. elif file_format == 'excel':
  238. return ExcelParser()
  239. elif file_format == 'audio':
  240. return AudioParser()
  241. else:
  242. raise ValidationError('format {} is invalid.'.format(file_format))
  243. class CloudUploadAPI(APIView):
  244. permission_classes = TextUploadAPI.permission_classes
  245. def get(self, request, *args, **kwargs):
  246. try:
  247. project_id = request.query_params['project_id']
  248. file_format = request.query_params['upload_format']
  249. cloud_container = request.query_params['container']
  250. cloud_object = request.query_params['object']
  251. except KeyError as ex:
  252. raise ValidationError('query parameter {} is missing'.format(ex))
  253. try:
  254. cloud_file = self.get_cloud_object_as_io(cloud_container, cloud_object)
  255. except ContainerDoesNotExistError:
  256. raise ValidationError('cloud container {} does not exist'.format(cloud_container))
  257. except ObjectDoesNotExistError:
  258. raise ValidationError('cloud object {} does not exist'.format(cloud_object))
  259. TextUploadAPI.save_file(
  260. user=request.user,
  261. file=cloud_file,
  262. file_format=file_format,
  263. project_id=project_id,
  264. )
  265. next_url = request.query_params.get('next')
  266. if next_url == 'about:blank':
  267. return Response(data='', content_type='text/plain', status=status.HTTP_201_CREATED)
  268. if next_url:
  269. return redirect(next_url)
  270. return Response(status=status.HTTP_201_CREATED)
  271. @classmethod
  272. def get_cloud_object_as_io(cls, container_name, object_name):
  273. provider = settings.CLOUD_BROWSER_APACHE_LIBCLOUD_PROVIDER.lower()
  274. account = settings.CLOUD_BROWSER_APACHE_LIBCLOUD_ACCOUNT
  275. key = settings.CLOUD_BROWSER_APACHE_LIBCLOUD_SECRET_KEY
  276. driver = get_driver(DriverType.STORAGE, provider)
  277. client = driver(account, key)
  278. cloud_container = client.get_container(container_name)
  279. cloud_object = cloud_container.get_object(object_name)
  280. return iterable_to_io(cloud_object.as_stream())
  281. class TextDownloadAPI(APIView):
  282. permission_classes = TextUploadAPI.permission_classes
  283. renderer_classes = (CSVRenderer, JSONLRenderer)
  284. def get(self, request, *args, **kwargs):
  285. format = request.query_params.get('q')
  286. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  287. documents = project.documents.all()
  288. painter = self.select_painter(format)
  289. # json1 format prints text labels while json format prints annotations with label ids
  290. # json1 format - "labels": [[0, 15, "PERSON"], ..]
  291. # json format - "annotations": [{"label": 5, "start_offset": 0, "end_offset": 2, "user": 1},..]
  292. if format == "json1":
  293. labels = project.labels.all()
  294. data = JSONPainter.paint_labels(documents, labels)
  295. else:
  296. data = painter.paint(documents)
  297. return Response(data)
  298. def select_painter(self, format):
  299. if format == 'csv':
  300. return CSVPainter()
  301. elif format == 'json' or format == "json1":
  302. return JSONPainter()
  303. else:
  304. raise ValidationError('format {} is invalid.'.format(format))
  305. class Users(APIView):
  306. permission_classes = [IsAuthenticated & IsProjectAdmin]
  307. def get(self, request, *args, **kwargs):
  308. queryset = User.objects.all()
  309. serialized_data = UserSerializer(queryset, many=True).data
  310. return Response(serialized_data)
  311. class Roles(generics.ListCreateAPIView):
  312. serializer_class = RoleSerializer
  313. pagination_class = None
  314. permission_classes = [IsAuthenticated & IsProjectAdmin]
  315. queryset = Role.objects.all()
  316. class RoleMappingList(generics.ListCreateAPIView):
  317. serializer_class = RoleMappingSerializer
  318. pagination_class = None
  319. permission_classes = [IsAuthenticated & IsProjectAdmin]
  320. def get_queryset(self):
  321. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  322. return project.role_mappings
  323. def perform_create(self, serializer):
  324. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  325. serializer.save(project=project)
  326. class RoleMappingDetail(generics.RetrieveUpdateDestroyAPIView):
  327. queryset = RoleMapping.objects.all()
  328. serializer_class = RoleMappingSerializer
  329. lookup_url_kwarg = 'rolemapping_id'
  330. permission_classes = [IsAuthenticated & IsProjectAdmin]
  331. class LabelUploadAPI(APIView):
  332. parser_classes = (MultiPartParser,)
  333. permission_classes = [IsAuthenticated & IsProjectAdmin]
  334. @transaction.atomic
  335. def post(self, request, *args, **kwargs):
  336. if 'file' not in request.data:
  337. raise ParseError('Empty content')
  338. labels = json.load(request.data['file'])
  339. project = get_object_or_404(Project, pk=kwargs['project_id'])
  340. try:
  341. for label in labels:
  342. serializer = LabelSerializer(data=label)
  343. serializer.is_valid(raise_exception=True)
  344. serializer.save(project=project)
  345. return Response(status=status.HTTP_201_CREATED)
  346. except IntegrityError:
  347. content = {'error': 'IntegrityError: you cannot create a label with same name or shortkey.'}
  348. return Response(content, status=status.HTTP_400_BAD_REQUEST)