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.

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