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.

635 lines
24 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
  1. import collections
  2. import json
  3. import random
  4. from auto_labeling_pipeline.menu import Options
  5. from auto_labeling_pipeline.models import RequestModelFactory
  6. from auto_labeling_pipeline.mappings import MappingTemplate
  7. from auto_labeling_pipeline.task import TaskFactory
  8. from auto_labeling_pipeline.postprocessing import PostProcessor
  9. from auto_labeling_pipeline.pipeline import pipeline
  10. from django.conf import settings
  11. from django.contrib.auth.models import User
  12. from django.db import transaction
  13. from django.db.utils import IntegrityError
  14. from django.shortcuts import get_object_or_404, redirect
  15. from django_filters.rest_framework import DjangoFilterBackend
  16. from django.db.models import Count, F, Q
  17. from libcloud.base import DriverType, get_driver
  18. from libcloud.storage.types import ContainerDoesNotExistError, ObjectDoesNotExistError
  19. from rest_framework import generics, filters, status
  20. from rest_framework.exceptions import ParseError, ValidationError
  21. from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
  22. from rest_framework.response import Response
  23. from rest_framework.views import APIView
  24. from rest_framework.parsers import MultiPartParser
  25. from rest_framework_csv.renderers import CSVRenderer
  26. from .filters import DocumentFilter
  27. from .models import Project, Label, Document, RoleMapping, Role, Comment, AutoLabelingConfig
  28. from .permissions import IsProjectAdmin, IsAnnotatorAndReadOnly, IsAnnotator, IsAnnotationApproverAndReadOnly, IsOwnAnnotation, IsAnnotationApprover, IsOwnComment
  29. from .serializers import ProjectSerializer, LabelSerializer, DocumentSerializer, UserSerializer, ApproverSerializer, CommentSerializer
  30. from .serializers import ProjectPolymorphicSerializer, RoleMappingSerializer, RoleSerializer, AutoLabelingConfigSerializer
  31. from .utils import CSVParser, ExcelParser, JSONParser, PlainTextParser, FastTextParser, CoNLLParser, AudioParser, iterable_to_io
  32. from .utils import JSONLRenderer, PlainTextRenderer
  33. from .utils import JSONPainter, CSVPainter, FastTextPainter
  34. IsInProjectReadOnlyOrAdmin = (IsAnnotatorAndReadOnly | IsAnnotationApproverAndReadOnly | IsProjectAdmin)
  35. IsInProjectOrAdmin = (IsAnnotator | IsAnnotationApprover | IsProjectAdmin)
  36. class Health(APIView):
  37. permission_classes = (IsAuthenticatedOrReadOnly,)
  38. def get(self, request, *args, **kwargs):
  39. return Response({'status': 'green'})
  40. class Me(APIView):
  41. permission_classes = (IsAuthenticated,)
  42. def get(self, request, *args, **kwargs):
  43. serializer = UserSerializer(request.user, context={'request': request})
  44. return Response(serializer.data)
  45. class Features(APIView):
  46. permission_classes = (IsAuthenticated,)
  47. def get(self, request, *args, **kwargs):
  48. return Response({
  49. 'cloud_upload': bool(settings.CLOUD_BROWSER_APACHE_LIBCLOUD_PROVIDER),
  50. })
  51. class ProjectList(generics.ListCreateAPIView):
  52. serializer_class = ProjectPolymorphicSerializer
  53. pagination_class = None
  54. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  55. def get_queryset(self):
  56. return self.request.user.projects
  57. def perform_create(self, serializer):
  58. serializer.save(users=[self.request.user])
  59. class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
  60. queryset = Project.objects.all()
  61. serializer_class = ProjectSerializer
  62. lookup_url_kwarg = 'project_id'
  63. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  64. class StatisticsAPI(APIView):
  65. pagination_class = None
  66. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  67. def get(self, request, *args, **kwargs):
  68. p = get_object_or_404(Project, pk=self.kwargs['project_id'])
  69. include = set(request.GET.getlist('include'))
  70. response = {}
  71. if not include or 'label' in include:
  72. label_count, user_count = self.label_per_data(p)
  73. response['label'] = label_count
  74. # TODO: Make user_label count chart
  75. response['user_label'] = user_count
  76. if not include or 'total' in include or 'remaining' in include or 'user' in include:
  77. progress = self.progress(project=p)
  78. response.update(progress)
  79. if include:
  80. response = {key: value for (key, value) in response.items() if key in include}
  81. return Response(response)
  82. @staticmethod
  83. def _get_user_completion_data(annotation_class, annotation_filter):
  84. all_annotation_objects = annotation_class.objects.filter(annotation_filter)
  85. set_user_data = collections.defaultdict(set)
  86. for ind_obj in all_annotation_objects.values('user__username', 'document__id'):
  87. set_user_data[ind_obj['user__username']].add(ind_obj['document__id'])
  88. return {i: len(set_user_data[i]) for i in set_user_data}
  89. def progress(self, project):
  90. docs = project.documents
  91. annotation_class = project.get_annotation_class()
  92. total = docs.count()
  93. annotation_filter = Q(document_id__in=docs.all())
  94. user_data = self._get_user_completion_data(annotation_class, annotation_filter)
  95. if not project.collaborative_annotation:
  96. annotation_filter &= Q(user_id=self.request.user)
  97. done = annotation_class.objects.filter(annotation_filter)\
  98. .aggregate(Count('document', distinct=True))['document__count']
  99. remaining = total - done
  100. return {'total': total, 'remaining': remaining, 'user': user_data}
  101. def label_per_data(self, project):
  102. annotation_class = project.get_annotation_class()
  103. return annotation_class.objects.get_label_per_data(project=project)
  104. class ApproveLabelsAPI(APIView):
  105. permission_classes = [IsAuthenticated & (IsAnnotationApprover | IsProjectAdmin)]
  106. def post(self, request, *args, **kwargs):
  107. approved = self.request.data.get('approved', True)
  108. document = get_object_or_404(Document, pk=self.kwargs['doc_id'])
  109. document.annotations_approved_by = self.request.user if approved else None
  110. document.save()
  111. return Response(ApproverSerializer(document).data)
  112. class LabelList(generics.ListCreateAPIView):
  113. serializer_class = LabelSerializer
  114. pagination_class = None
  115. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  116. def get_queryset(self):
  117. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  118. return project.labels
  119. def perform_create(self, serializer):
  120. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  121. serializer.save(project=project)
  122. class LabelDetail(generics.RetrieveUpdateDestroyAPIView):
  123. queryset = Label.objects.all()
  124. serializer_class = LabelSerializer
  125. lookup_url_kwarg = 'label_id'
  126. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  127. class DocumentList(generics.ListCreateAPIView):
  128. serializer_class = DocumentSerializer
  129. filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
  130. search_fields = ('text', )
  131. ordering_fields = ('created_at', 'updated_at', 'doc_annotations__updated_at',
  132. 'seq_annotations__updated_at', 'seq2seq_annotations__updated_at')
  133. filter_class = DocumentFilter
  134. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  135. def get_queryset(self):
  136. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  137. queryset = project.documents
  138. if project.randomize_document_order:
  139. random.seed(self.request.user.id)
  140. value = random.randrange(2, 20)
  141. queryset = queryset.annotate(sort_id=F('id') % value).order_by('sort_id', 'id')
  142. else:
  143. queryset = queryset.order_by('id')
  144. return queryset
  145. def perform_create(self, serializer):
  146. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  147. serializer.save(project=project)
  148. def delete(self, request, *args, **kwargs):
  149. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  150. queryset = project.documents
  151. queryset.all().delete()
  152. return Response(status=status.HTTP_204_NO_CONTENT)
  153. class DocumentDetail(generics.RetrieveUpdateDestroyAPIView):
  154. queryset = Document.objects.all()
  155. serializer_class = DocumentSerializer
  156. lookup_url_kwarg = 'doc_id'
  157. permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
  158. class AnnotationList(generics.ListCreateAPIView):
  159. pagination_class = None
  160. permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
  161. swagger_schema = None
  162. def get_serializer_class(self):
  163. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  164. self.serializer_class = project.get_annotation_serializer()
  165. return self.serializer_class
  166. def get_queryset(self):
  167. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  168. model = project.get_annotation_class()
  169. queryset = model.objects.filter(document=self.kwargs['doc_id'])
  170. if not project.collaborative_annotation:
  171. queryset = queryset.filter(user=self.request.user)
  172. return queryset
  173. def create(self, request, *args, **kwargs):
  174. self.check_single_class_classification(self.kwargs['project_id'], self.kwargs['doc_id'], request.user)
  175. request.data['document'] = self.kwargs['doc_id']
  176. return super().create(request, args, kwargs)
  177. def perform_create(self, serializer):
  178. serializer.save(document_id=self.kwargs['doc_id'], user=self.request.user)
  179. def delete(self, request, *args, **kwargs):
  180. queryset = self.get_queryset()
  181. queryset.all().delete()
  182. return Response(status=status.HTTP_204_NO_CONTENT)
  183. @staticmethod
  184. def check_single_class_classification(project_id, doc_id, user):
  185. project = get_object_or_404(Project, pk=project_id)
  186. if not project.single_class_classification:
  187. return
  188. model = project.get_annotation_class()
  189. annotations = model.objects.filter(document_id=doc_id)
  190. if not project.collaborative_annotation:
  191. annotations = annotations.filter(user=user)
  192. if annotations.exists():
  193. raise ValidationError('requested to create duplicate annotation for single-class-classification project')
  194. class AnnotationDetail(generics.RetrieveUpdateDestroyAPIView):
  195. lookup_url_kwarg = 'annotation_id'
  196. swagger_schema = None
  197. def get_permissions(self):
  198. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  199. if project.collaborative_annotation:
  200. self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
  201. else:
  202. self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin & IsOwnAnnotation]
  203. return super().get_permissions()
  204. def get_serializer_class(self):
  205. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  206. self.serializer_class = project.get_annotation_serializer()
  207. return self.serializer_class
  208. def get_queryset(self):
  209. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  210. model = project.get_annotation_class()
  211. self.queryset = model.objects.all()
  212. return self.queryset
  213. class CommentList(generics.ListCreateAPIView):
  214. serializer_class = CommentSerializer
  215. permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
  216. def get_queryset(self):
  217. return Comment.objects.filter(
  218. document_id=self.kwargs['doc_id'],
  219. user_id=self.request.user.id,
  220. ).all()
  221. def perform_create(self, serializer):
  222. serializer.save(user=self.request.user, document_id=self.kwargs['doc_id'])
  223. class CommentDetail(generics.RetrieveUpdateDestroyAPIView):
  224. queryset = Comment.objects.all()
  225. serializer_class = CommentSerializer
  226. lookup_url_kwarg = 'comment_id'
  227. permission_classes = [IsAuthenticated & IsInProjectOrAdmin & IsOwnComment]
  228. class TextUploadAPI(APIView):
  229. parser_classes = (MultiPartParser,)
  230. permission_classes = [IsAuthenticated & IsProjectAdmin]
  231. def post(self, request, *args, **kwargs):
  232. if 'file' not in request.data:
  233. raise ParseError('Empty content')
  234. self.save_file(
  235. user=request.user,
  236. file=request.data['file'],
  237. file_format=request.data['format'],
  238. project_id=kwargs['project_id'],
  239. )
  240. return Response(status=status.HTTP_201_CREATED)
  241. @classmethod
  242. def save_file(cls, user, file, file_format, project_id):
  243. project = get_object_or_404(Project, pk=project_id)
  244. parser = cls.select_parser(file_format)
  245. data = parser.parse(file)
  246. storage = project.get_storage(data)
  247. storage.save(user)
  248. @classmethod
  249. def select_parser(cls, file_format):
  250. if file_format == 'plain':
  251. return PlainTextParser()
  252. elif file_format == 'csv':
  253. return CSVParser()
  254. elif file_format == 'json':
  255. return JSONParser()
  256. elif file_format == 'conll':
  257. return CoNLLParser()
  258. elif file_format == 'excel':
  259. return ExcelParser()
  260. elif file_format == 'audio':
  261. return AudioParser()
  262. elif file_format == 'fastText':
  263. return FastTextParser()
  264. else:
  265. raise ValidationError('format {} is invalid.'.format(file_format))
  266. class CloudUploadAPI(APIView):
  267. permission_classes = TextUploadAPI.permission_classes
  268. def get(self, request, *args, **kwargs):
  269. try:
  270. project_id = request.query_params['project_id']
  271. file_format = request.query_params['upload_format']
  272. cloud_container = request.query_params['container']
  273. cloud_object = request.query_params['object']
  274. except KeyError as ex:
  275. raise ValidationError('query parameter {} is missing'.format(ex))
  276. try:
  277. cloud_file = self.get_cloud_object_as_io(cloud_container, cloud_object)
  278. except ContainerDoesNotExistError:
  279. raise ValidationError('cloud container {} does not exist'.format(cloud_container))
  280. except ObjectDoesNotExistError:
  281. raise ValidationError('cloud object {} does not exist'.format(cloud_object))
  282. TextUploadAPI.save_file(
  283. user=request.user,
  284. file=cloud_file,
  285. file_format=file_format,
  286. project_id=project_id,
  287. )
  288. next_url = request.query_params.get('next')
  289. if next_url == 'about:blank':
  290. return Response(data='', content_type='text/plain', status=status.HTTP_201_CREATED)
  291. if next_url:
  292. return redirect(next_url)
  293. return Response(status=status.HTTP_201_CREATED)
  294. @classmethod
  295. def get_cloud_object_as_io(cls, container_name, object_name):
  296. provider = settings.CLOUD_BROWSER_APACHE_LIBCLOUD_PROVIDER.lower()
  297. account = settings.CLOUD_BROWSER_APACHE_LIBCLOUD_ACCOUNT
  298. key = settings.CLOUD_BROWSER_APACHE_LIBCLOUD_SECRET_KEY
  299. driver = get_driver(DriverType.STORAGE, provider)
  300. client = driver(account, key)
  301. cloud_container = client.get_container(container_name)
  302. cloud_object = cloud_container.get_object(object_name)
  303. return iterable_to_io(cloud_object.as_stream())
  304. class TextDownloadAPI(APIView):
  305. permission_classes = TextUploadAPI.permission_classes
  306. renderer_classes = (CSVRenderer, JSONLRenderer, PlainTextRenderer)
  307. def get(self, request, *args, **kwargs):
  308. format = request.query_params.get('q')
  309. only_approved = request.query_params.get('onlyApproved')
  310. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  311. documents = (
  312. project.documents.exclude(annotations_approved_by = None)
  313. if only_approved == 'true'
  314. else project.documents.all()
  315. )
  316. painter = self.select_painter(format)
  317. # jsonl-textlabel format prints text labels while jsonl format prints annotations with label ids
  318. # jsonl-textlabel format - "labels": [[0, 15, "PERSON"], ..]
  319. # jsonl format - "annotations": [{"label": 5, "start_offset": 0, "end_offset": 2, "user": 1},..]
  320. if format in ('jsonl', 'txt'):
  321. labels = project.labels.all()
  322. data = painter.paint_labels(documents, labels)
  323. else:
  324. data = painter.paint(documents)
  325. return Response(data)
  326. def select_painter(self, format):
  327. if format == 'csv':
  328. return CSVPainter()
  329. elif format == 'jsonl' or format == 'json':
  330. return JSONPainter()
  331. elif format == 'txt':
  332. return FastTextPainter()
  333. else:
  334. raise ValidationError('format {} is invalid.'.format(format))
  335. class Users(APIView):
  336. permission_classes = [IsAuthenticated & IsProjectAdmin]
  337. def get(self, request, *args, **kwargs):
  338. queryset = User.objects.all()
  339. serialized_data = UserSerializer(queryset, many=True).data
  340. return Response(serialized_data)
  341. class Roles(generics.ListCreateAPIView):
  342. serializer_class = RoleSerializer
  343. pagination_class = None
  344. permission_classes = [IsAuthenticated & IsProjectAdmin]
  345. queryset = Role.objects.all()
  346. class RoleMappingList(generics.ListCreateAPIView):
  347. serializer_class = RoleMappingSerializer
  348. pagination_class = None
  349. permission_classes = [IsAuthenticated & IsProjectAdmin]
  350. def get_queryset(self):
  351. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  352. return project.role_mappings
  353. def perform_create(self, serializer):
  354. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  355. serializer.save(project=project)
  356. class RoleMappingDetail(generics.RetrieveUpdateDestroyAPIView):
  357. queryset = RoleMapping.objects.all()
  358. serializer_class = RoleMappingSerializer
  359. lookup_url_kwarg = 'rolemapping_id'
  360. permission_classes = [IsAuthenticated & IsProjectAdmin]
  361. class LabelUploadAPI(APIView):
  362. parser_classes = (MultiPartParser,)
  363. permission_classes = [IsAuthenticated & IsProjectAdmin]
  364. @transaction.atomic
  365. def post(self, request, *args, **kwargs):
  366. if 'file' not in request.data:
  367. raise ParseError('Empty content')
  368. labels = json.load(request.data['file'])
  369. project = get_object_or_404(Project, pk=kwargs['project_id'])
  370. try:
  371. for label in labels:
  372. serializer = LabelSerializer(data=label)
  373. serializer.is_valid(raise_exception=True)
  374. serializer.save(project=project)
  375. return Response(status=status.HTTP_201_CREATED)
  376. except IntegrityError:
  377. content = {'error': 'IntegrityError: you cannot create a label with same name or shortkey.'}
  378. return Response(content, status=status.HTTP_400_BAD_REQUEST)
  379. class AutoLabelingTemplateListAPI(APIView):
  380. permission_classes = [IsAuthenticated & IsProjectAdmin]
  381. def get(self, request, *args, **kwargs):
  382. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  383. options = Options.filter_by_task(task_name=project.project_type)
  384. option_names = [o.name for o in options]
  385. return Response(option_names, status=status.HTTP_200_OK)
  386. class AutoLabelingTemplateDetailAPI(APIView):
  387. permission_classes = [IsAuthenticated & IsProjectAdmin]
  388. def get(self, request, *args, **kwargs):
  389. option_name = self.kwargs['option_name']
  390. option = Options.find(option_name=option_name)
  391. return Response(option.to_dict(), status=status.HTTP_200_OK)
  392. class AutoLabelingConfigList(generics.ListCreateAPIView):
  393. serializer_class = AutoLabelingConfigSerializer
  394. pagination_class = None
  395. permission_classes = [IsAuthenticated & IsProjectAdmin]
  396. def get_queryset(self):
  397. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  398. return project.auto_labeling_config
  399. def perform_create(self, serializer):
  400. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  401. serializer.save(project=project)
  402. class AutoLabelingConfigDetail(generics.RetrieveUpdateDestroyAPIView):
  403. queryset = AutoLabelingConfig.objects.all()
  404. serializer_class = AutoLabelingConfigSerializer
  405. lookup_url_kwarg = 'config_id'
  406. permission_classes = [IsAuthenticated & IsProjectAdmin]
  407. class AutoLabelingConfigTest(APIView):
  408. permission_classes = [IsAuthenticated & IsProjectAdmin]
  409. def post(self, *args, **kwargs):
  410. try:
  411. output = self.pass_config_validation()
  412. output = self.pass_pipeline_call(output)
  413. return Response(
  414. data={'valid': True, 'labels': output.dict()},
  415. status=status.HTTP_200_OK
  416. )
  417. except Exception:
  418. return Response(
  419. data={'valid': False},
  420. status=status.HTTP_400_BAD_REQUEST
  421. )
  422. def pass_config_validation(self):
  423. config = self.request.data['config']
  424. serializer = AutoLabelingConfigSerializer(data=config)
  425. serializer.is_valid(raise_exception=True)
  426. return serializer
  427. def pass_pipeline_call(self, serializer):
  428. test_input = self.request.data['input']
  429. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  430. model = RequestModelFactory.create(
  431. model_name=serializer.data.get('model_name'),
  432. attributes=serializer.data.get('model_attrs')
  433. )
  434. template = MappingTemplate(
  435. task=TaskFactory.create(project.project_type),
  436. template=serializer.data.get('template')
  437. )
  438. post_processor = PostProcessor(serializer.data.get('label_mapping'))
  439. labels = pipeline(
  440. text=test_input,
  441. request_model=model,
  442. mapping_template=template,
  443. post_processing=post_processor
  444. )
  445. return labels
  446. class AutoLabelingAnnotation(generics.CreateAPIView):
  447. pagination_class = None
  448. permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
  449. swagger_schema = None
  450. def get_serializer_class(self):
  451. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  452. self.serializer_class = project.get_annotation_serializer()
  453. return self.serializer_class
  454. def get_queryset(self):
  455. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  456. model = project.get_annotation_class()
  457. queryset = model.objects.filter(document=self.kwargs['doc_id'])
  458. if not project.collaborative_annotation:
  459. queryset = queryset.filter(user=self.request.user)
  460. return queryset
  461. def create(self, request, *args, **kwargs):
  462. labels = self.extract()
  463. labels = self.transform(labels)
  464. serializer = self.get_serializer(data=labels, many=True)
  465. serializer.is_valid(raise_exception=True)
  466. self.perform_create(serializer)
  467. headers = self.get_success_headers(serializer.data)
  468. return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
  469. def perform_create(self, serializer):
  470. serializer.save(user=self.request.user)
  471. def extract(self):
  472. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  473. doc = get_object_or_404(Document, pk=self.kwargs['doc_id'])
  474. config = project.auto_labeling_config.get(default=True)
  475. model = RequestModelFactory.create(
  476. model_name=config.model_name,
  477. attributes=config.model_attrs
  478. )
  479. template = MappingTemplate(
  480. task=TaskFactory.create(project.project_type),
  481. template=config.template
  482. )
  483. post_processor = PostProcessor(config.label_mapping)
  484. labels = pipeline(
  485. text=doc.text,
  486. request_model=model,
  487. mapping_template=template,
  488. post_processing=post_processor
  489. )
  490. return labels.dict()
  491. def transform(self, labels):
  492. project = get_object_or_404(Project, pk=self.kwargs['project_id'])
  493. for label in labels:
  494. label['document'] = self.kwargs['doc_id']
  495. if 'label' in label:
  496. label['label'] = project.labels.get(text=label.pop('label')).id
  497. return labels