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.

1567 lines
79 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. import os
  2. from django.conf import settings
  3. from django.test import override_settings
  4. from rest_framework import status
  5. from rest_framework.reverse import reverse
  6. from rest_framework.test import APITestCase
  7. from model_mommy import mommy
  8. from ..models import User, SequenceAnnotation, Document, Role, RoleMapping
  9. from ..models import DOCUMENT_CLASSIFICATION, SEQUENCE_LABELING, SEQ2SEQ
  10. from ..utils import PlainTextParser, CoNLLParser, JSONParser, CSVParser
  11. from ..exceptions import FileParseException
  12. DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
  13. def create_default_roles():
  14. Role.objects.get_or_create(name=settings.ROLE_PROJECT_ADMIN)
  15. Role.objects.get_or_create(name=settings.ROLE_ANNOTATOR)
  16. Role.objects.get_or_create(name=settings.ROLE_ANNOTATION_APPROVER)
  17. def assign_user_to_role(project_member, project, role_name):
  18. role, _ = Role.objects.get_or_create(name=role_name)
  19. RoleMapping.objects.get_or_create(role_id=role.id, user_id=project_member.id, project_id=project.id)
  20. def remove_all_role_mappings():
  21. RoleMapping.objects.all().delete()
  22. class TestUtilsMixin:
  23. def _patch_project(self, project, attribute, value):
  24. old_value = getattr(project, attribute, None)
  25. setattr(project, attribute, value)
  26. project.save()
  27. def cleanup_project():
  28. setattr(project, attribute, old_value)
  29. project.save()
  30. self.addCleanup(cleanup_project)
  31. @override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
  32. class TestProjectListAPI(APITestCase):
  33. @classmethod
  34. def setUpTestData(cls):
  35. cls.main_project_member_name = 'project_member_name'
  36. cls.main_project_member_pass = 'project_member_pass'
  37. cls.sub_project_member_name = 'sub_project_member_name'
  38. cls.sub_project_member_pass = 'sub_project_member_pass'
  39. cls.approver_name = 'approver_name_name'
  40. cls.approver_pass = 'approver_pass'
  41. cls.super_user_name = 'super_user_name'
  42. cls.super_user_pass = 'super_user_pass'
  43. create_default_roles()
  44. main_project_member = User.objects.create_user(username=cls.main_project_member_name,
  45. password=cls.main_project_member_pass)
  46. sub_project_member = User.objects.create_user(username=cls.sub_project_member_name,
  47. password=cls.sub_project_member_pass)
  48. approver = User.objects.create_user(username=cls.approver_name,
  49. password=cls.approver_pass)
  50. User.objects.create_superuser(username=cls.super_user_name,
  51. password=cls.super_user_pass,
  52. email='fizz@buzz.com')
  53. cls.main_project = mommy.make('TextClassificationProject', users=[main_project_member])
  54. cls.sub_project = mommy.make('TextClassificationProject', users=[sub_project_member])
  55. assign_user_to_role(project_member=main_project_member, project=cls.main_project,
  56. role_name=settings.ROLE_ANNOTATOR)
  57. assign_user_to_role(project_member=sub_project_member, project=cls.sub_project,
  58. role_name=settings.ROLE_ANNOTATOR)
  59. assign_user_to_role(project_member=approver, project=cls.main_project,
  60. role_name=settings.ROLE_ANNOTATION_APPROVER)
  61. cls.url = reverse(viewname='project_list')
  62. cls.data = {'name': 'example', 'project_type': 'DocumentClassification',
  63. 'description': 'example', 'guideline': 'example',
  64. 'resourcetype': 'TextClassificationProject'}
  65. cls.num_project = main_project_member.projects.count()
  66. def test_returns_main_project_to_approver(self):
  67. self.client.login(username=self.approver_name,
  68. password=self.approver_pass)
  69. response = self.client.get(self.url, format='json')
  70. project = response.data[0]
  71. num_project = len(response.data)
  72. self.assertEqual(num_project, self.num_project)
  73. self.assertEqual(project['id'], self.main_project.id)
  74. def test_returns_main_project_to_main_project_member(self):
  75. self.client.login(username=self.main_project_member_name,
  76. password=self.main_project_member_pass)
  77. response = self.client.get(self.url, format='json')
  78. project = response.data[0]
  79. num_project = len(response.data)
  80. self.assertEqual(num_project, self.num_project)
  81. self.assertEqual(project['id'], self.main_project.id)
  82. def test_do_not_return_main_project_to_sub_project_member(self):
  83. self.client.login(username=self.sub_project_member_name,
  84. password=self.sub_project_member_pass)
  85. response = self.client.get(self.url, format='json')
  86. project = response.data[0]
  87. num_project = len(response.data)
  88. self.assertEqual(num_project, self.num_project)
  89. self.assertNotEqual(project['id'], self.main_project.id)
  90. def test_allows_superuser_to_create_project(self):
  91. self.client.login(username=self.super_user_name,
  92. password=self.super_user_pass)
  93. response = self.client.post(self.url, format='json', data=self.data)
  94. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  95. def test_disallows_project_member_to_create_project(self):
  96. self.client.login(username=self.main_project_member_name,
  97. password=self.main_project_member_pass)
  98. response = self.client.post(self.url, format='json', data=self.data)
  99. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  100. @classmethod
  101. def doCleanups(cls):
  102. remove_all_role_mappings()
  103. @override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
  104. class TestProjectDetailAPI(APITestCase):
  105. @classmethod
  106. def setUpTestData(cls):
  107. cls.project_member_name = 'project_member_name'
  108. cls.project_member_pass = 'project_member_pass'
  109. cls.non_project_member_name = 'non_project_member_name'
  110. cls.non_project_member_pass = 'non_project_member_pass'
  111. cls.admin_user_name = 'admin_user_name'
  112. cls.admin_user_pass = 'admin_user_pass'
  113. create_default_roles()
  114. cls.project_member = User.objects.create_user(username=cls.project_member_name,
  115. password=cls.project_member_pass)
  116. non_project_member = User.objects.create_user(username=cls.non_project_member_name,
  117. password=cls.non_project_member_pass)
  118. project_admin = User.objects.create_superuser(username=cls.admin_user_name,
  119. password=cls.admin_user_pass,
  120. email='fizz@buzz.com')
  121. cls.main_project = mommy.make('TextClassificationProject', users=[cls.project_member, project_admin])
  122. mommy.make('TextClassificationProject', users=[non_project_member])
  123. cls.url = reverse(viewname='project_detail', args=[cls.main_project.id])
  124. cls.data = {'description': 'lorem'}
  125. assign_user_to_role(project_member=cls.project_member, project=cls.main_project,
  126. role_name=settings.ROLE_ANNOTATOR)
  127. assign_user_to_role(project_member=project_admin, project=cls.main_project,
  128. role_name=settings.ROLE_PROJECT_ADMIN)
  129. def test_returns_main_project_detail_to_main_project_member(self):
  130. self.client.login(username=self.project_member_name,
  131. password=self.project_member_pass)
  132. response = self.client.get(self.url, format='json')
  133. self.assertEqual(response.data['id'], self.main_project.id)
  134. def test_do_not_return_main_project_to_sub_project_member(self):
  135. self.client.login(username=self.non_project_member_name,
  136. password=self.non_project_member_pass)
  137. response = self.client.get(self.url, format='json')
  138. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  139. def test_allows_admin_to_update_project(self):
  140. self.client.login(username=self.admin_user_name,
  141. password=self.admin_user_pass)
  142. response = self.client.patch(self.url, format='json', data=self.data)
  143. self.assertEqual(response.data['description'], self.data['description'])
  144. def test_disallows_non_project_member_to_update_project(self):
  145. self.client.login(username=self.non_project_member_name,
  146. password=self.non_project_member_pass)
  147. response = self.client.patch(self.url, format='json', data=self.data)
  148. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  149. def test_allows_admin_to_delete_project(self):
  150. self.client.login(username=self.admin_user_name,
  151. password=self.admin_user_pass)
  152. response = self.client.delete(self.url, format='json')
  153. self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
  154. def test_disallows_non_project_member_to_delete_project(self):
  155. self.client.login(username=self.non_project_member_name,
  156. password=self.non_project_member_pass)
  157. response = self.client.delete(self.url, format='json')
  158. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  159. @classmethod
  160. def doCleanups(cls):
  161. remove_all_role_mappings()
  162. class TestLabelListAPI(APITestCase):
  163. @classmethod
  164. def setUpTestData(cls):
  165. cls.project_member_name = 'project_member_name'
  166. cls.project_member_pass = 'project_member_pass'
  167. cls.non_project_member_name = 'non_project_member_name'
  168. cls.non_project_member_pass = 'non_project_member_pass'
  169. cls.admin_user_name = 'admin_user_name'
  170. cls.admin_user_pass = 'admin_user_pass'
  171. create_default_roles()
  172. cls.project_member = User.objects.create_user(username=cls.project_member_name,
  173. password=cls.project_member_pass)
  174. non_project_member = User.objects.create_user(username=cls.non_project_member_name,
  175. password=cls.non_project_member_pass)
  176. project_admin = User.objects.create_superuser(username=cls.admin_user_name,
  177. password=cls.admin_user_pass,
  178. email='fizz@buzz.com')
  179. cls.main_project = mommy.make('Project', users=[cls.project_member, project_admin])
  180. cls.main_project_label = mommy.make('Label', project=cls.main_project)
  181. sub_project = mommy.make('Project', users=[non_project_member])
  182. other_project = mommy.make('Project', users=[project_admin])
  183. mommy.make('Label', project=sub_project)
  184. cls.url = reverse(viewname='label_list', args=[cls.main_project.id])
  185. cls.other_url = reverse(viewname='label_list', args=[other_project.id])
  186. cls.data = {'text': 'example'}
  187. assign_user_to_role(project_member=cls.project_member, project=cls.main_project,
  188. role_name=settings.ROLE_ANNOTATOR)
  189. def test_returns_labels_to_project_member(self):
  190. self.client.login(username=self.project_member_name,
  191. password=self.project_member_pass)
  192. response = self.client.get(self.url, format='json')
  193. self.assertEqual(response.status_code, status.HTTP_200_OK)
  194. def test_do_not_return_labels_to_non_project_member(self):
  195. self.client.login(username=self.non_project_member_name,
  196. password=self.non_project_member_pass)
  197. response = self.client.get(self.url, format='json')
  198. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  199. def test_do_not_return_labels_of_other_projects(self):
  200. self.client.login(username=self.project_member_name,
  201. password=self.project_member_pass)
  202. response = self.client.get(self.url, format='json')
  203. label = response.data[0]
  204. num_labels = len(response.data)
  205. self.assertEqual(num_labels, len(self.main_project.labels.all()))
  206. self.assertEqual(label['id'], self.main_project_label.id)
  207. def test_allows_admin_to_create_label(self):
  208. self.client.login(username=self.admin_user_name,
  209. password=self.admin_user_pass)
  210. response = self.client.post(self.url, format='json', data=self.data)
  211. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  212. def test_can_create_multiple_labels_without_shortcut_key(self):
  213. self.client.login(username=self.admin_user_name,
  214. password=self.admin_user_pass)
  215. labels = [
  216. {'text': 'Ruby', 'prefix_key': None, 'suffix_key': None},
  217. {'text': 'PHP', 'prefix_key': None, 'suffix_key': None}
  218. ]
  219. for label in labels:
  220. response = self.client.post(self.url, format='json', data=label)
  221. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  222. def test_can_create_same_label_in_multiple_projects(self):
  223. self.client.login(username=self.admin_user_name,
  224. password=self.admin_user_pass)
  225. label = {'text': 'LOC', 'prefix_key': None, 'suffix_key': 'l'}
  226. response = self.client.post(self.url, format='json', data=label)
  227. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  228. response = self.client.post(self.other_url, format='json', data=label)
  229. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  230. def test_can_create_same_suffix_with_different_prefix(self):
  231. self.client.login(username=self.admin_user_name,
  232. password=self.admin_user_pass)
  233. label = {'text': 'Person', 'prefix_key': None, 'suffix_key': 'p'}
  234. response = self.client.post(self.url, format='json', data=label)
  235. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  236. label = {'text': 'Percentage', 'prefix_key': 'ctrl', 'suffix_key': 'p'}
  237. response = self.client.post(self.url, format='json', data=label)
  238. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  239. def test_cannot_create_same_shortcut_key(self):
  240. self.client.login(username=self.admin_user_name,
  241. password=self.admin_user_pass)
  242. label = {'text': 'Person', 'prefix_key': None, 'suffix_key': 'p'}
  243. response = self.client.post(self.url, format='json', data=label)
  244. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  245. label = {'text': 'Percentage', 'prefix_key': None, 'suffix_key': 'p'}
  246. response = self.client.post(self.url, format='json', data=label)
  247. self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
  248. def test_disallows_project_member_to_create_label(self):
  249. self.client.login(username=self.project_member_name,
  250. password=self.project_member_pass)
  251. response = self.client.post(self.url, format='json', data=self.data)
  252. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  253. @classmethod
  254. def doCleanups(cls):
  255. remove_all_role_mappings()
  256. class TestLabelDetailAPI(APITestCase):
  257. @classmethod
  258. def setUpTestData(cls):
  259. cls.project_member_name = 'project_member_name'
  260. cls.project_member_pass = 'project_member_pass'
  261. cls.non_project_member_name = 'non_project_member_name'
  262. cls.non_project_member_pass = 'non_project_member_pass'
  263. cls.super_user_name = 'super_user_name'
  264. cls.super_user_pass = 'super_user_pass'
  265. create_default_roles()
  266. project_member = User.objects.create_user(username=cls.project_member_name,
  267. password=cls.project_member_pass)
  268. User.objects.create_user(username=cls.non_project_member_name, password=cls.non_project_member_pass)
  269. # Todo: change super_user to project_admin.
  270. super_user = User.objects.create_superuser(username=cls.super_user_name,
  271. password=cls.super_user_pass,
  272. email='fizz@buzz.com')
  273. project = mommy.make('Project', users=[project_member, super_user])
  274. cls.label = mommy.make('Label', project=project)
  275. cls.label_with_shortcut = mommy.make('Label', suffix_key='l', project=project)
  276. cls.url = reverse(viewname='label_detail', args=[project.id, cls.label.id])
  277. cls.url_with_shortcut = reverse(viewname='label_detail', args=[project.id, cls.label_with_shortcut.id])
  278. cls.data = {'text': 'example'}
  279. create_default_roles()
  280. assign_user_to_role(project_member=project_member, project=project,
  281. role_name=settings.ROLE_ANNOTATOR)
  282. def test_returns_label_to_project_member(self):
  283. self.client.login(username=self.project_member_name,
  284. password=self.project_member_pass)
  285. response = self.client.get(self.url, format='json')
  286. self.assertEqual(response.data['id'], self.label.id)
  287. def test_do_not_return_label_to_non_project_member(self):
  288. self.client.login(username=self.non_project_member_name,
  289. password=self.non_project_member_pass)
  290. response = self.client.get(self.url, format='json')
  291. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  292. def test_allows_superuser_to_update_label(self):
  293. self.client.login(username=self.super_user_name,
  294. password=self.super_user_pass)
  295. response = self.client.patch(self.url, format='json', data=self.data)
  296. self.assertEqual(response.data['text'], self.data['text'])
  297. def test_allows_superuser_to_update_label_with_shortcut(self):
  298. self.client.login(username=self.super_user_name,
  299. password=self.super_user_pass)
  300. response = self.client.patch(self.url_with_shortcut, format='json', data={'suffix_key': 's'})
  301. self.assertEqual(response.data['suffix_key'], 's')
  302. def test_disallows_project_member_to_update_label(self):
  303. self.client.login(username=self.project_member_name,
  304. password=self.project_member_pass)
  305. response = self.client.patch(self.url, format='json', data=self.data)
  306. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  307. def test_allows_superuser_to_delete_label(self):
  308. self.client.login(username=self.super_user_name,
  309. password=self.super_user_pass)
  310. response = self.client.delete(self.url, format='json')
  311. self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
  312. def test_disallows_project_member_to_delete_label(self):
  313. self.client.login(username=self.project_member_name,
  314. password=self.project_member_pass)
  315. response = self.client.delete(self.url, format='json')
  316. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  317. @classmethod
  318. def doCleanups(cls):
  319. remove_all_role_mappings()
  320. class TestDocumentListAPI(APITestCase):
  321. @classmethod
  322. def setUpTestData(cls):
  323. cls.project_member_name = 'project_member_name'
  324. cls.project_member_pass = 'project_member_pass'
  325. cls.non_project_member_name = 'non_project_member_name'
  326. cls.non_project_member_pass = 'non_project_member_pass'
  327. cls.super_user_name = 'super_user_name'
  328. cls.super_user_pass = 'super_user_pass'
  329. create_default_roles()
  330. project_member = User.objects.create_user(username=cls.project_member_name,
  331. password=cls.project_member_pass)
  332. non_project_member = User.objects.create_user(username=cls.non_project_member_name,
  333. password=cls.non_project_member_pass)
  334. super_user = User.objects.create_superuser(username=cls.super_user_name,
  335. password=cls.super_user_pass,
  336. email='fizz@buzz.com')
  337. cls.main_project = mommy.make('TextClassificationProject', users=[project_member, super_user])
  338. doc1 = mommy.make('Document', project=cls.main_project)
  339. doc2 = mommy.make('Document', project=cls.main_project)
  340. mommy.make('Document', project=cls.main_project)
  341. cls.random_order_project = mommy.make('TextClassificationProject', users=[project_member, super_user],
  342. randomize_document_order=True)
  343. mommy.make('Document', 100, project=cls.random_order_project)
  344. sub_project = mommy.make('TextClassificationProject', users=[non_project_member])
  345. mommy.make('Document', project=sub_project)
  346. cls.url = reverse(viewname='doc_list', args=[cls.main_project.id])
  347. cls.random_order_project_url = reverse(viewname='doc_list', args=[cls.random_order_project.id])
  348. cls.data = {'text': 'example'}
  349. assign_user_to_role(project_member=project_member, project=cls.main_project,
  350. role_name=settings.ROLE_ANNOTATOR)
  351. assign_user_to_role(project_member=project_member, project=cls.random_order_project,
  352. role_name=settings.ROLE_ANNOTATOR)
  353. mommy.make('DocumentAnnotation', document=doc1, user=project_member)
  354. mommy.make('DocumentAnnotation', document=doc2, user=project_member)
  355. def test_returns_docs_to_project_member(self):
  356. self.client.login(username=self.project_member_name,
  357. password=self.project_member_pass)
  358. response = self.client.get(self.url, format='json')
  359. self.assertEqual(response.status_code, status.HTTP_200_OK)
  360. self.assertEqual(len(response.json().get('results')), 3)
  361. def test_returns_docs_to_project_member_filtered_to_active(self):
  362. self.client.login(username=self.project_member_name,
  363. password=self.project_member_pass)
  364. response = self.client.get('{}?doc_annotations__isnull=true'.format(self.url), format='json')
  365. self.assertEqual(response.status_code, status.HTTP_200_OK)
  366. self.assertEqual(len(response.json().get('results')), 1)
  367. def test_returns_docs_to_project_member_filtered_to_completed(self):
  368. self.client.login(username=self.project_member_name,
  369. password=self.project_member_pass)
  370. response = self.client.get('{}?doc_annotations__isnull=false'.format(self.url), format='json')
  371. self.assertEqual(response.status_code, status.HTTP_200_OK)
  372. self.assertEqual(len(response.json().get('results')), 2)
  373. def test_returns_docs_in_consistent_order_for_all_users(self):
  374. self.client.login(username=self.project_member_name, password=self.project_member_pass)
  375. user1_documents = self.client.get(self.url, format='json').json().get('results')
  376. self.client.logout()
  377. self.client.login(username=self.super_user_name, password=self.super_user_pass)
  378. user2_documents = self.client.get(self.url, format='json').json().get('results')
  379. self.client.logout()
  380. self.assertEqual([doc['id'] for doc in user1_documents], [doc['id'] for doc in user2_documents])
  381. def test_can_return_docs_in_consistent_random_order(self):
  382. self.client.login(username=self.project_member_name, password=self.project_member_pass)
  383. user1_documents1 = self.client.get(self.random_order_project_url, format='json').json().get('results')
  384. user1_documents2 = self.client.get(self.random_order_project_url, format='json').json().get('results')
  385. self.client.logout()
  386. self.assertEqual(user1_documents1, user1_documents2)
  387. self.client.login(username=self.super_user_name, password=self.super_user_pass)
  388. user2_documents1 = self.client.get(self.random_order_project_url, format='json').json().get('results')
  389. user2_documents2 = self.client.get(self.random_order_project_url, format='json').json().get('results')
  390. self.client.logout()
  391. self.assertEqual(user2_documents1, user2_documents2)
  392. self.assertNotEqual(user1_documents1, user2_documents1)
  393. self.assertNotEqual(user1_documents2, user2_documents2)
  394. def test_do_not_return_docs_to_non_project_member(self):
  395. self.client.login(username=self.non_project_member_name,
  396. password=self.non_project_member_pass)
  397. response = self.client.get(self.url, format='json')
  398. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  399. def test_do_not_return_docs_of_other_projects(self):
  400. self.client.login(username=self.project_member_name,
  401. password=self.project_member_pass)
  402. response = self.client.get(self.url, format='json')
  403. self.assertEqual(response.data['count'], self.main_project.documents.count())
  404. def test_allows_superuser_to_create_doc(self):
  405. self.client.login(username=self.super_user_name,
  406. password=self.super_user_pass)
  407. response = self.client.post(self.url, format='json', data=self.data)
  408. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  409. def test_disallows_project_member_to_create_doc(self):
  410. self.client.login(username=self.project_member_name,
  411. password=self.project_member_pass)
  412. response = self.client.post(self.url, format='json', data=self.data)
  413. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  414. @classmethod
  415. def doCleanups(cls):
  416. remove_all_role_mappings()
  417. class TestDocumentDetailAPI(APITestCase):
  418. @classmethod
  419. def setUpTestData(cls):
  420. cls.project_member_name = 'project_member_name'
  421. cls.project_member_pass = 'project_member_pass'
  422. cls.non_project_member_name = 'non_project_member_name'
  423. cls.non_project_member_pass = 'non_project_member_pass'
  424. cls.super_user_name = 'super_user_name'
  425. cls.super_user_pass = 'super_user_pass'
  426. create_default_roles()
  427. project_member = User.objects.create_user(username=cls.project_member_name,
  428. password=cls.project_member_pass)
  429. non_project_member = User.objects.create_user(username=cls.non_project_member_name,
  430. password=cls.non_project_member_pass)
  431. # Todo: change super_user to project_admin.
  432. super_user = User.objects.create_superuser(username=cls.super_user_name,
  433. password=cls.super_user_pass,
  434. email='fizz@buzz.com')
  435. project = mommy.make('TextClassificationProject', users=[project_member, super_user])
  436. cls.doc = mommy.make('Document', project=project)
  437. cls.url = reverse(viewname='doc_detail', args=[project.id, cls.doc.id])
  438. cls.data = {'text': 'example'}
  439. assign_user_to_role(project_member=project_member, project=project,
  440. role_name=settings.ROLE_ANNOTATOR)
  441. def test_returns_doc_to_project_member(self):
  442. self.client.login(username=self.project_member_name,
  443. password=self.project_member_pass)
  444. response = self.client.get(self.url, format='json')
  445. self.assertEqual(response.data['id'], self.doc.id)
  446. def test_do_not_return_doc_to_non_project_member(self):
  447. self.client.login(username=self.non_project_member_name,
  448. password=self.non_project_member_pass)
  449. response = self.client.get(self.url, format='json')
  450. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  451. def test_allows_superuser_to_update_doc(self):
  452. self.client.login(username=self.super_user_name,
  453. password=self.super_user_pass)
  454. response = self.client.patch(self.url, format='json', data=self.data)
  455. self.assertEqual(response.data['text'], self.data['text'])
  456. def test_disallows_project_member_to_update_doc(self):
  457. self.client.login(username=self.project_member_name,
  458. password=self.project_member_pass)
  459. response = self.client.patch(self.url, format='json', data=self.data)
  460. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  461. def test_allows_superuser_to_delete_doc(self):
  462. self.client.login(username=self.super_user_name,
  463. password=self.super_user_pass)
  464. response = self.client.delete(self.url, format='json')
  465. self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
  466. def test_disallows_project_member_to_delete_doc(self):
  467. self.client.login(username=self.project_member_name,
  468. password=self.project_member_pass)
  469. response = self.client.delete(self.url, format='json')
  470. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  471. @classmethod
  472. def doCleanups(cls):
  473. remove_all_role_mappings()
  474. class TestApproveLabelsAPI(APITestCase):
  475. @classmethod
  476. def setUpTestData(cls):
  477. cls.annotator_name = 'annotator_name'
  478. cls.annotator_pass = 'annotator_pass'
  479. cls.approver_name = 'approver_name_name'
  480. cls.approver_pass = 'approver_pass'
  481. cls.project_admin_name = 'project_admin_name'
  482. cls.project_admin_pass = 'project_admin_pass'
  483. annotator = User.objects.create_user(username=cls.annotator_name,
  484. password=cls.annotator_pass)
  485. approver = User.objects.create_user(username=cls.approver_name,
  486. password=cls.approver_pass)
  487. project_admin = User.objects.create_user(username=cls.project_admin_name,
  488. password=cls.project_admin_pass)
  489. project = mommy.make('TextClassificationProject', users=[annotator, approver, project_admin])
  490. cls.doc = mommy.make('Document', project=project)
  491. cls.url = reverse(viewname='approve_labels', args=[project.id, cls.doc.id])
  492. create_default_roles()
  493. assign_user_to_role(project_member=annotator, project=project,
  494. role_name=settings.ROLE_ANNOTATOR)
  495. assign_user_to_role(project_member=approver, project=project,
  496. role_name=settings.ROLE_ANNOTATION_APPROVER)
  497. assign_user_to_role(project_member=project_admin, project=project,
  498. role_name=settings.ROLE_PROJECT_ADMIN)
  499. def test_allow_project_admin_to_approve_and_disapprove_labels(self):
  500. self.client.login(username=self.project_admin_name, password=self.project_admin_pass)
  501. response = self.client.post(self.url, format='json', data={'approved': True})
  502. self.assertEqual(response.data['annotation_approver'], self.project_admin_name)
  503. response = self.client.post(self.url, format='json', data={'approved': False})
  504. self.assertIsNone(response.data['annotation_approver'])
  505. def test_allow_approver_to_approve_and_disapprove_labels(self):
  506. self.client.login(username=self.approver_name, password=self.approver_pass)
  507. response = self.client.post(self.url, format='json', data={'approved': True})
  508. self.assertEqual(response.data['annotation_approver'], self.approver_name)
  509. response = self.client.post(self.url, format='json', data={'approved': False})
  510. self.assertIsNone(response.data['annotation_approver'])
  511. def test_disallows_non_annotation_approver_to_approve_and_disapprove_labels(self):
  512. self.client.login(username=self.annotator_name, password=self.annotator_pass)
  513. response = self.client.post(self.url, format='json', data={'approved': True})
  514. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  515. @classmethod
  516. def doCleanups(cls):
  517. remove_all_role_mappings()
  518. class TestAnnotationListAPI(APITestCase, TestUtilsMixin):
  519. @classmethod
  520. def setUpTestData(cls):
  521. cls.project_member_name = 'project_member_name'
  522. cls.project_member_pass = 'project_member_pass'
  523. cls.another_project_member_name = 'another_project_member_name'
  524. cls.another_project_member_pass = 'another_project_member_pass'
  525. cls.non_project_member_name = 'non_project_member_name'
  526. cls.non_project_member_pass = 'non_project_member_pass'
  527. create_default_roles()
  528. project_member = User.objects.create_user(username=cls.project_member_name,
  529. password=cls.project_member_pass)
  530. another_project_member = User.objects.create_user(username=cls.another_project_member_name,
  531. password=cls.another_project_member_pass)
  532. non_project_member = User.objects.create_user(username=cls.non_project_member_name,
  533. password=cls.non_project_member_pass)
  534. main_project = mommy.make('SequenceLabelingProject', users=[project_member, another_project_member])
  535. main_project_label = mommy.make('Label', project=main_project)
  536. main_project_doc = mommy.make('Document', project=main_project)
  537. mommy.make('SequenceAnnotation', document=main_project_doc, user=project_member)
  538. mommy.make('SequenceAnnotation', document=main_project_doc, user=another_project_member)
  539. sub_project = mommy.make('SequenceLabelingProject', users=[non_project_member])
  540. sub_project_doc = mommy.make('Document', project=sub_project)
  541. mommy.make('SequenceAnnotation', document=sub_project_doc)
  542. cls.url = reverse(viewname='annotation_list', args=[main_project.id, main_project_doc.id])
  543. cls.post_data = {'start_offset': 0, 'end_offset': 1, 'label': main_project_label.id}
  544. cls.num_entity_of_project_member = SequenceAnnotation.objects.filter(document=main_project_doc,
  545. user=project_member).count()
  546. cls.num_entity_of_another_project_member = SequenceAnnotation.objects.filter(
  547. document=main_project_doc,
  548. user=another_project_member).count()
  549. cls.main_project = main_project
  550. assign_user_to_role(project_member=project_member, project=main_project,
  551. role_name=settings.ROLE_ANNOTATOR)
  552. def test_returns_annotations_to_project_member(self):
  553. self.client.login(username=self.project_member_name,
  554. password=self.project_member_pass)
  555. response = self.client.get(self.url, format='json')
  556. self.assertEqual(response.status_code, status.HTTP_200_OK)
  557. def test_do_not_return_annotations_to_non_project_member(self):
  558. self.client.login(username=self.non_project_member_name,
  559. password=self.non_project_member_pass)
  560. response = self.client.get(self.url, format='json')
  561. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  562. def test_do_not_return_annotations_of_another_project_member(self):
  563. self.client.login(username=self.project_member_name,
  564. password=self.project_member_pass)
  565. response = self.client.get(self.url, format='json')
  566. self.assertEqual(len(response.data), self.num_entity_of_project_member)
  567. def test_returns_annotations_of_another_project_member_if_collaborative_project(self):
  568. self._patch_project(self.main_project, 'collaborative_annotation', True)
  569. self.client.login(username=self.project_member_name,
  570. password=self.project_member_pass)
  571. response = self.client.get(self.url, format='json')
  572. self.assertEqual(len(response.data),
  573. self.num_entity_of_project_member + self.num_entity_of_another_project_member)
  574. def test_allows_project_member_to_create_annotation(self):
  575. self.client.login(username=self.project_member_name,
  576. password=self.project_member_pass)
  577. response = self.client.post(self.url, format='json', data=self.post_data)
  578. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  579. def test_disallows_non_project_member_to_create_annotation(self):
  580. self.client.login(username=self.non_project_member_name,
  581. password=self.non_project_member_pass)
  582. response = self.client.post(self.url, format='json', data=self.post_data)
  583. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  584. def _patch_project(self, project, attribute, value):
  585. old_value = getattr(project, attribute, None)
  586. setattr(project, attribute, value)
  587. project.save()
  588. def cleanup_project():
  589. setattr(project, attribute, old_value)
  590. project.save()
  591. self.addCleanup(cleanup_project)
  592. @classmethod
  593. def doCleanups(cls):
  594. remove_all_role_mappings()
  595. class TestAnnotationDetailAPI(APITestCase):
  596. @classmethod
  597. def setUpTestData(cls):
  598. cls.super_user_name = 'super_user_name'
  599. cls.super_user_pass = 'super_user_pass'
  600. cls.project_member_name = 'project_member_name'
  601. cls.project_member_pass = 'project_member_pass'
  602. cls.another_project_member_name = 'another_project_member_name'
  603. cls.another_project_member_pass = 'another_project_member_pass'
  604. cls.non_project_member_name = 'non_project_member_name'
  605. cls.non_project_member_pass = 'non_project_member_pass'
  606. # Todo: change super_user to project_admin.
  607. super_user = User.objects.create_superuser(username=cls.super_user_name,
  608. password=cls.super_user_pass,
  609. email='fizz@buzz.com')
  610. create_default_roles()
  611. project_member = User.objects.create_user(username=cls.project_member_name,
  612. password=cls.project_member_pass)
  613. another_project_member = User.objects.create_user(username=cls.another_project_member_name,
  614. password=cls.another_project_member_pass)
  615. non_project_member = User.objects.create_user(username=cls.non_project_member_name,
  616. password=cls.non_project_member_pass)
  617. main_project = mommy.make('SequenceLabelingProject',
  618. users=[super_user, project_member, another_project_member])
  619. main_project_doc = mommy.make('Document', project=main_project)
  620. main_project_entity = mommy.make('SequenceAnnotation',
  621. document=main_project_doc, user=project_member)
  622. another_entity = mommy.make('SequenceAnnotation',
  623. document=main_project_doc, user=another_project_member)
  624. sub_project = mommy.make('SequenceLabelingProject', users=[non_project_member])
  625. sub_project_doc = mommy.make('Document', project=sub_project)
  626. mommy.make('SequenceAnnotation', document=sub_project_doc)
  627. cls.url = reverse(viewname='annotation_detail', args=[main_project.id,
  628. main_project_doc.id,
  629. main_project_entity.id])
  630. cls.another_url = reverse(viewname='annotation_detail', args=[main_project.id,
  631. main_project_doc.id,
  632. another_entity.id])
  633. cls.post_data = {'start_offset': 0, 'end_offset': 10}
  634. assign_user_to_role(project_member=project_member, project=main_project,
  635. role_name=settings.ROLE_ANNOTATOR)
  636. def test_returns_annotation_to_project_member(self):
  637. self.client.login(username=self.project_member_name,
  638. password=self.project_member_pass)
  639. response = self.client.get(self.url, format='json')
  640. self.assertEqual(response.status_code, status.HTTP_200_OK)
  641. def test_do_not_return_annotation_to_non_project_member(self):
  642. self.client.login(username=self.non_project_member_name,
  643. password=self.non_project_member_pass)
  644. response = self.client.get(self.url, format='json')
  645. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  646. def test_do_not_return_annotation_by_another_project_member(self):
  647. self.client.login(username=self.project_member_name,
  648. password=self.project_member_pass)
  649. response = self.client.get(self.another_url, format='json')
  650. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  651. def test_allows_project_member_to_update_annotation(self):
  652. self.client.login(username=self.project_member_name,
  653. password=self.project_member_pass)
  654. response = self.client.patch(self.url, format='json', data=self.post_data)
  655. self.assertEqual(response.status_code, status.HTTP_200_OK)
  656. def test_disallows_non_project_member_to_update_annotation(self):
  657. self.client.login(username=self.non_project_member_name,
  658. password=self.non_project_member_pass)
  659. response = self.client.patch(self.url, format='json', data=self.post_data)
  660. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  661. def test_disallows_project_member_to_update_annotation_of_another_member(self):
  662. self.client.login(username=self.project_member_name,
  663. password=self.project_member_pass)
  664. response = self.client.patch(self.another_url, format='json', data=self.post_data)
  665. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  666. def test_allows_superuser_to_delete_annotation_of_another_member(self):
  667. self.client.login(username=self.super_user_name,
  668. password=self.super_user_pass)
  669. response = self.client.delete(self.another_url, format='json', data=self.post_data)
  670. self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
  671. def test_allows_project_member_to_delete_annotation(self):
  672. self.client.login(username=self.project_member_name,
  673. password=self.project_member_pass)
  674. response = self.client.delete(self.url, format='json')
  675. self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
  676. def test_disallows_project_member_to_delete_annotation(self):
  677. self.client.login(username=self.non_project_member_name,
  678. password=self.non_project_member_pass)
  679. response = self.client.delete(self.url, format='json')
  680. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  681. def test_disallows_project_member_to_delete_annotation_of_another_member(self):
  682. self.client.login(username=self.project_member_name,
  683. password=self.project_member_pass)
  684. response = self.client.delete(self.another_url, format='json', data=self.post_data)
  685. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  686. @classmethod
  687. def doCleanups(cls):
  688. remove_all_role_mappings()
  689. class TestSearch(APITestCase):
  690. @classmethod
  691. def setUpTestData(cls):
  692. cls.project_member_name = 'project_member_name'
  693. cls.project_member_pass = 'project_member_pass'
  694. cls.non_project_member_name = 'non_project_member_name'
  695. cls.non_project_member_pass = 'non_project_member_pass'
  696. create_default_roles()
  697. project_member = User.objects.create_user(username=cls.project_member_name,
  698. password=cls.project_member_pass)
  699. non_project_member = User.objects.create_user(username=cls.non_project_member_name,
  700. password=cls.non_project_member_pass)
  701. cls.main_project = mommy.make('TextClassificationProject', users=[project_member])
  702. cls.search_term = 'example'
  703. doc1 = mommy.make('Document', text=cls.search_term, project=cls.main_project)
  704. doc2 = mommy.make('Document', text='Lorem', project=cls.main_project)
  705. label1 = mommy.make('Label', project=cls.main_project)
  706. label2 = mommy.make('Label', project=cls.main_project)
  707. mommy.make('SequenceAnnotation', document=doc1, user=project_member, label=label1)
  708. mommy.make('SequenceAnnotation', document=doc2, user=project_member, label=label2)
  709. sub_project = mommy.make('TextClassificationProject', users=[non_project_member])
  710. mommy.make('Document', text=cls.search_term, project=sub_project)
  711. cls.url = reverse(viewname='doc_list', args=[cls.main_project.id])
  712. cls.data = {'q': cls.search_term}
  713. assign_user_to_role(project_member=project_member, project=cls.main_project,
  714. role_name=settings.ROLE_ANNOTATOR)
  715. def test_can_filter_doc_by_term(self):
  716. self.client.login(username=self.project_member_name,
  717. password=self.project_member_pass)
  718. response = self.client.get(self.url, format='json', data=self.data)
  719. count = Document.objects.filter(text__contains=self.search_term,
  720. project=self.main_project).count()
  721. self.assertEqual(response.data['count'], count)
  722. def test_can_order_doc_by_created_at_ascending(self):
  723. params = {'ordering': 'created_at'}
  724. self.client.login(username=self.project_member_name,
  725. password=self.project_member_pass)
  726. response = self.client.get(self.url, format='json', data=params)
  727. docs = Document.objects.filter(project=self.main_project).order_by('created_at').values()
  728. for d1, d2 in zip(response.data['results'], docs):
  729. self.assertEqual(d1['id'], d2['id'])
  730. def test_can_order_doc_by_created_at_descending(self):
  731. params = {'ordering': '-created_at'}
  732. self.client.login(username=self.project_member_name,
  733. password=self.project_member_pass)
  734. response = self.client.get(self.url, format='json', data=params)
  735. docs = Document.objects.filter(project=self.main_project).order_by('-created_at').values()
  736. for d1, d2 in zip(response.data['results'], docs):
  737. self.assertEqual(d1['id'], d2['id'])
  738. def test_can_order_doc_by_annotation_updated_at_ascending(self):
  739. params = {'ordering': 'seq_annotations__updated_at'}
  740. self.client.login(username=self.project_member_name,
  741. password=self.project_member_pass)
  742. response = self.client.get(self.url, format='json', data=params)
  743. docs = Document.objects.filter(project=self.main_project).order_by('seq_annotations__updated_at').values()
  744. for d1, d2 in zip(response.data['results'], docs):
  745. self.assertEqual(d1['id'], d2['id'])
  746. def test_can_order_doc_by_annotation_updated_at_descending(self):
  747. params = {'ordering': '-seq_annotations__updated_at'}
  748. self.client.login(username=self.project_member_name,
  749. password=self.project_member_pass)
  750. response = self.client.get(self.url, format='json', data=params)
  751. docs = Document.objects.filter(project=self.main_project).order_by('-seq_annotations__updated_at').values()
  752. for d1, d2 in zip(response.data['results'], docs):
  753. self.assertEqual(d1['id'], d2['id'])
  754. @classmethod
  755. def doCleanups(cls):
  756. remove_all_role_mappings()
  757. class TestFilter(APITestCase):
  758. @classmethod
  759. def setUpTestData(cls):
  760. cls.project_member_name = 'project_member_name'
  761. cls.project_member_pass = 'project_member_pass'
  762. create_default_roles()
  763. project_member = User.objects.create_user(username=cls.project_member_name,
  764. password=cls.project_member_pass)
  765. cls.main_project = mommy.make('SequenceLabelingProject', users=[project_member])
  766. cls.label1 = mommy.make('Label', project=cls.main_project)
  767. cls.label2 = mommy.make('Label', project=cls.main_project)
  768. doc1 = mommy.make('Document', project=cls.main_project)
  769. doc2 = mommy.make('Document', project=cls.main_project)
  770. mommy.make('Document', project=cls.main_project)
  771. mommy.make('SequenceAnnotation', document=doc1, user=project_member, label=cls.label1)
  772. mommy.make('SequenceAnnotation', document=doc2, user=project_member, label=cls.label2)
  773. cls.url = reverse(viewname='doc_list', args=[cls.main_project.id])
  774. cls.params = {'seq_annotations__label__id': cls.label1.id}
  775. assign_user_to_role(project_member=project_member, project=cls.main_project,
  776. role_name=settings.ROLE_ANNOTATOR)
  777. def test_can_filter_by_label(self):
  778. self.client.login(username=self.project_member_name,
  779. password=self.project_member_pass)
  780. response = self.client.get(self.url, format='json', data=self.params)
  781. docs = Document.objects.filter(project=self.main_project,
  782. seq_annotations__label__id=self.label1.id).values()
  783. for d1, d2 in zip(response.data['results'], docs):
  784. self.assertEqual(d1['id'], d2['id'])
  785. def test_can_filter_doc_with_annotation(self):
  786. params = {'seq_annotations__isnull': False}
  787. self.client.login(username=self.project_member_name,
  788. password=self.project_member_pass)
  789. response = self.client.get(self.url, format='json', data=params)
  790. docs = Document.objects.filter(project=self.main_project, seq_annotations__isnull=False).values()
  791. self.assertEqual(response.data['count'], docs.count())
  792. for d1, d2 in zip(response.data['results'], docs):
  793. self.assertEqual(d1['id'], d2['id'])
  794. def test_can_filter_doc_without_anotation(self):
  795. params = {'seq_annotations__isnull': True}
  796. self.client.login(username=self.project_member_name,
  797. password=self.project_member_pass)
  798. response = self.client.get(self.url, format='json', data=params)
  799. docs = Document.objects.filter(project=self.main_project, seq_annotations__isnull=True).values()
  800. self.assertEqual(response.data['count'], docs.count())
  801. for d1, d2 in zip(response.data['results'], docs):
  802. self.assertEqual(d1['id'], d2['id'])
  803. @classmethod
  804. def doCleanups(cls):
  805. remove_all_role_mappings()
  806. class TestUploader(APITestCase):
  807. @classmethod
  808. def setUpTestData(cls):
  809. cls.super_user_name = 'super_user_name'
  810. cls.super_user_pass = 'super_user_pass'
  811. # Todo: change super_user to project_admin.
  812. create_default_roles()
  813. super_user = User.objects.create_superuser(username=cls.super_user_name,
  814. password=cls.super_user_pass,
  815. email='fizz@buzz.com')
  816. cls.classification_project = mommy.make('TextClassificationProject',
  817. users=[super_user], project_type=DOCUMENT_CLASSIFICATION)
  818. cls.labeling_project = mommy.make('SequenceLabelingProject',
  819. users=[super_user], project_type=SEQUENCE_LABELING)
  820. cls.seq2seq_project = mommy.make('Seq2seqProject', users=[super_user], project_type=SEQ2SEQ)
  821. assign_user_to_role(project_member=super_user, project=cls.classification_project,
  822. role_name=settings.ROLE_PROJECT_ADMIN)
  823. assign_user_to_role(project_member=super_user, project=cls.labeling_project,
  824. role_name=settings.ROLE_PROJECT_ADMIN)
  825. assign_user_to_role(project_member=super_user, project=cls.seq2seq_project,
  826. role_name=settings.ROLE_PROJECT_ADMIN)
  827. def setUp(self):
  828. self.client.login(username=self.super_user_name,
  829. password=self.super_user_pass)
  830. def upload_test_helper(self, project_id, filename, file_format, expected_status, **kwargs):
  831. url = reverse(viewname='doc_uploader', args=[project_id])
  832. with open(os.path.join(DATA_DIR, filename), 'rb') as f:
  833. response = self.client.post(url, data={'file': f, 'format': file_format})
  834. self.assertEqual(response.status_code, expected_status)
  835. def label_test_helper(self, project_id, expected_labels, expected_label_keys):
  836. url = reverse(viewname='label_list', args=[project_id])
  837. expected_keys = {key for label in expected_labels for key in label}
  838. response = self.client.get(url).json()
  839. actual_labels = [{key: value for (key, value) in label.items() if key in expected_keys}
  840. for label in response]
  841. self.assertCountEqual(actual_labels, expected_labels)
  842. for label in response:
  843. for expected_label_key in expected_label_keys:
  844. self.assertIsNotNone(label.get(expected_label_key))
  845. def test_can_upload_conll_format_file(self):
  846. self.upload_test_helper(project_id=self.labeling_project.id,
  847. filename='labeling.conll',
  848. file_format='conll',
  849. expected_status=status.HTTP_201_CREATED)
  850. def test_cannot_upload_wrong_conll_format_file(self):
  851. self.upload_test_helper(project_id=self.labeling_project.id,
  852. filename='labeling.invalid.conll',
  853. file_format='conll',
  854. expected_status=status.HTTP_400_BAD_REQUEST)
  855. def test_can_upload_classification_csv(self):
  856. self.upload_test_helper(project_id=self.classification_project.id,
  857. filename='example.csv',
  858. file_format='csv',
  859. expected_status=status.HTTP_201_CREATED)
  860. def test_can_upload_csv_with_non_utf8_encoding(self):
  861. self.upload_test_helper(project_id=self.classification_project.id,
  862. filename='example.utf16.csv',
  863. file_format='csv',
  864. expected_status=status.HTTP_201_CREATED)
  865. def test_can_upload_seq2seq_csv(self):
  866. self.upload_test_helper(project_id=self.seq2seq_project.id,
  867. filename='example.csv',
  868. file_format='csv',
  869. expected_status=status.HTTP_201_CREATED)
  870. def test_can_upload_single_column_csv(self):
  871. self.upload_test_helper(project_id=self.seq2seq_project.id,
  872. filename='example_one_column.csv',
  873. file_format='csv',
  874. expected_status=status.HTTP_201_CREATED)
  875. def test_cannot_upload_csv_file_does_not_match_column_and_row(self):
  876. self.upload_test_helper(project_id=self.classification_project.id,
  877. filename='example.invalid.1.csv',
  878. file_format='csv',
  879. expected_status=status.HTTP_400_BAD_REQUEST)
  880. def test_cannot_upload_csv_file_has_too_many_columns(self):
  881. self.upload_test_helper(project_id=self.classification_project.id,
  882. filename='example.invalid.2.csv',
  883. file_format='csv',
  884. expected_status=status.HTTP_400_BAD_REQUEST)
  885. def test_can_upload_classification_excel(self):
  886. self.upload_test_helper(project_id=self.classification_project.id,
  887. filename='example.xlsx',
  888. file_format='excel',
  889. expected_status=status.HTTP_201_CREATED)
  890. def test_can_upload_seq2seq_excel(self):
  891. self.upload_test_helper(project_id=self.seq2seq_project.id,
  892. filename='example.xlsx',
  893. file_format='excel',
  894. expected_status=status.HTTP_201_CREATED)
  895. def test_can_upload_single_column_excel(self):
  896. self.upload_test_helper(project_id=self.seq2seq_project.id,
  897. filename='example_one_column.xlsx',
  898. file_format='excel',
  899. expected_status=status.HTTP_201_CREATED)
  900. def test_cannot_upload_excel_file_does_not_match_column_and_row(self):
  901. self.upload_test_helper(project_id=self.classification_project.id,
  902. filename='example.invalid.1.xlsx',
  903. file_format='excel',
  904. expected_status=status.HTTP_400_BAD_REQUEST)
  905. def test_cannot_upload_excel_file_has_too_many_columns(self):
  906. self.upload_test_helper(project_id=self.classification_project.id,
  907. filename='example.invalid.2.xlsx',
  908. file_format='excel',
  909. expected_status=status.HTTP_400_BAD_REQUEST)
  910. @override_settings(IMPORT_BATCH_SIZE=1)
  911. def test_can_upload_small_batch_size(self):
  912. self.upload_test_helper(project_id=self.seq2seq_project.id,
  913. filename='example_one_column_no_header.xlsx',
  914. file_format='excel',
  915. expected_status=status.HTTP_201_CREATED)
  916. def test_can_upload_classification_jsonl(self):
  917. self.upload_test_helper(project_id=self.classification_project.id,
  918. filename='classification.jsonl',
  919. file_format='json',
  920. expected_status=status.HTTP_201_CREATED)
  921. self.label_test_helper(
  922. project_id=self.classification_project.id,
  923. expected_labels=[
  924. {'text': 'positive', 'suffix_key': 'p', 'prefix_key': None},
  925. {'text': 'negative', 'suffix_key': 'n', 'prefix_key': None},
  926. {'text': 'neutral', 'suffix_key': 'n', 'prefix_key': 'ctrl'},
  927. ],
  928. expected_label_keys=[
  929. 'background_color',
  930. 'text_color',
  931. ])
  932. def test_can_upload_labeling_jsonl(self):
  933. self.upload_test_helper(project_id=self.labeling_project.id,
  934. filename='labeling.jsonl',
  935. file_format='json',
  936. expected_status=status.HTTP_201_CREATED)
  937. self.label_test_helper(
  938. project_id=self.labeling_project.id,
  939. expected_labels=[
  940. {'text': 'LOC', 'suffix_key': 'l', 'prefix_key': None},
  941. {'text': 'ORG', 'suffix_key': 'o', 'prefix_key': None},
  942. {'text': 'PER', 'suffix_key': 'p', 'prefix_key': None},
  943. ],
  944. expected_label_keys=[
  945. 'background_color',
  946. 'text_color',
  947. ])
  948. def test_can_upload_seq2seq_jsonl(self):
  949. self.upload_test_helper(project_id=self.seq2seq_project.id,
  950. filename='seq2seq.jsonl',
  951. file_format='json',
  952. expected_status=status.HTTP_201_CREATED)
  953. def test_can_upload_plain_text(self):
  954. self.upload_test_helper(project_id=self.classification_project.id,
  955. filename='example.txt',
  956. file_format='plain',
  957. expected_status=status.HTTP_201_CREATED)
  958. def test_can_upload_data_without_label(self):
  959. self.upload_test_helper(project_id=self.classification_project.id,
  960. filename='example.jsonl',
  961. file_format='json',
  962. expected_status=status.HTTP_201_CREATED)
  963. @classmethod
  964. def doCleanups(cls):
  965. remove_all_role_mappings()
  966. @override_settings(CLOUD_BROWSER_APACHE_LIBCLOUD_PROVIDER='LOCAL')
  967. @override_settings(CLOUD_BROWSER_APACHE_LIBCLOUD_ACCOUNT=os.path.dirname(DATA_DIR))
  968. @override_settings(CLOUD_BROWSER_APACHE_LIBCLOUD_SECRET_KEY='not-used')
  969. class TestCloudUploader(TestUploader):
  970. def upload_test_helper(self, project_id, filename, file_format, expected_status, **kwargs):
  971. query_params = {
  972. 'project_id': project_id,
  973. 'upload_format': file_format,
  974. 'container': kwargs.pop('container', os.path.basename(DATA_DIR)),
  975. 'object': filename,
  976. }
  977. query_params.update(kwargs)
  978. response = self.client.get(reverse('cloud_uploader'), query_params)
  979. self.assertEqual(response.status_code, expected_status)
  980. def test_cannot_upload_with_missing_file(self):
  981. self.upload_test_helper(project_id=self.classification_project.id,
  982. filename='does-not-exist',
  983. file_format='json',
  984. expected_status=status.HTTP_400_BAD_REQUEST)
  985. def test_cannot_upload_with_missing_container(self):
  986. self.upload_test_helper(project_id=self.classification_project.id,
  987. filename='example.jsonl',
  988. container='does-not-exist',
  989. file_format='json',
  990. expected_status=status.HTTP_400_BAD_REQUEST)
  991. def test_cannot_upload_with_missing_query_parameters(self):
  992. response = self.client.get(reverse('cloud_uploader'), {'project_id': self.classification_project.id})
  993. self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
  994. def test_can_upload_with_redirect(self):
  995. self.upload_test_helper(project_id=self.classification_project.id,
  996. filename='example.jsonl',
  997. next='http://somewhere',
  998. file_format='json',
  999. expected_status=status.HTTP_302_FOUND)
  1000. def test_can_upload_with_redirect_to_blank(self):
  1001. self.upload_test_helper(project_id=self.classification_project.id,
  1002. filename='example.jsonl',
  1003. next='about:blank',
  1004. file_format='json',
  1005. expected_status=status.HTTP_201_CREATED)
  1006. class TestFeatures(APITestCase):
  1007. @classmethod
  1008. def setUpTestData(cls):
  1009. cls.user_name = 'user_name'
  1010. cls.user_pass = 'user_pass'
  1011. create_default_roles()
  1012. cls.user = User.objects.create_user(username=cls.user_name, password=cls.user_pass, email='fizz@buzz.com')
  1013. def setUp(self):
  1014. self.client.login(username=self.user_name, password=self.user_pass)
  1015. @override_settings(CLOUD_BROWSER_APACHE_LIBCLOUD_PROVIDER=None)
  1016. def test_no_cloud_upload(self):
  1017. response = self.client.get(reverse('features'))
  1018. self.assertFalse(response.json().get('cloud_upload'))
  1019. @override_settings(IMPORT_BATCH_SIZE=2)
  1020. class TestParser(APITestCase):
  1021. def parser_helper(self, filename, parser, include_label=True):
  1022. with open(os.path.join(DATA_DIR, filename), mode='rb') as f:
  1023. result = list(parser.parse(f))
  1024. for data in result:
  1025. for r in data:
  1026. self.assertIn('text', r)
  1027. if include_label:
  1028. self.assertIn('labels', r)
  1029. return result
  1030. def test_give_valid_data_to_conll_parser(self):
  1031. self.parser_helper(filename='labeling.conll', parser=CoNLLParser())
  1032. def test_give_valid_data_to_conll_parser_with_trailing_newlines(self):
  1033. result = self.parser_helper(filename='labeling.trailing.conll', parser=CoNLLParser())
  1034. self.assertEqual(len(result), 1)
  1035. self.assertEqual(len(result[0]), 1)
  1036. def test_plain_parser(self):
  1037. self.parser_helper(filename='example.txt', parser=PlainTextParser(), include_label=False)
  1038. def test_give_invalid_data_to_conll_parser(self):
  1039. with self.assertRaises(FileParseException):
  1040. self.parser_helper(filename='labeling.invalid.conll',
  1041. parser=CoNLLParser())
  1042. def test_give_classification_data_to_csv_parser(self):
  1043. self.parser_helper(filename='example.csv', parser=CSVParser())
  1044. def test_give_seq2seq_data_to_csv_parser(self):
  1045. self.parser_helper(filename='example.csv', parser=CSVParser())
  1046. def test_give_classification_data_to_json_parser(self):
  1047. self.parser_helper(filename='classification.jsonl', parser=JSONParser())
  1048. def test_give_labeling_data_to_json_parser(self):
  1049. self.parser_helper(filename='labeling.jsonl', parser=JSONParser())
  1050. def test_give_seq2seq_data_to_json_parser(self):
  1051. self.parser_helper(filename='seq2seq.jsonl', parser=JSONParser())
  1052. def test_give_data_without_label_to_json_parser(self):
  1053. self.parser_helper(filename='example.jsonl', parser=JSONParser(), include_label=False)
  1054. class TestDownloader(APITestCase):
  1055. @classmethod
  1056. def setUpTestData(cls):
  1057. cls.super_user_name = 'super_user_name'
  1058. cls.super_user_pass = 'super_user_pass'
  1059. # Todo: change super_user to project_admin.
  1060. create_default_roles()
  1061. super_user = User.objects.create_superuser(username=cls.super_user_name,
  1062. password=cls.super_user_pass,
  1063. email='fizz@buzz.com')
  1064. cls.classification_project = mommy.make('TextClassificationProject',
  1065. users=[super_user], project_type=DOCUMENT_CLASSIFICATION)
  1066. cls.labeling_project = mommy.make('SequenceLabelingProject',
  1067. users=[super_user], project_type=SEQUENCE_LABELING)
  1068. cls.seq2seq_project = mommy.make('Seq2seqProject', users=[super_user], project_type=SEQ2SEQ)
  1069. cls.classification_url = reverse(viewname='doc_downloader', args=[cls.classification_project.id])
  1070. cls.labeling_url = reverse(viewname='doc_downloader', args=[cls.labeling_project.id])
  1071. cls.seq2seq_url = reverse(viewname='doc_downloader', args=[cls.seq2seq_project.id])
  1072. def setUp(self):
  1073. self.client.login(username=self.super_user_name,
  1074. password=self.super_user_pass)
  1075. def download_test_helper(self, url, format, expected_status):
  1076. response = self.client.get(url, data={'q': format})
  1077. self.assertEqual(response.status_code, expected_status)
  1078. def test_cannot_download_conll_format_file(self):
  1079. self.download_test_helper(url=self.labeling_url,
  1080. format='conll',
  1081. expected_status=status.HTTP_400_BAD_REQUEST)
  1082. def test_can_download_classification_csv(self):
  1083. self.download_test_helper(url=self.classification_url,
  1084. format='csv',
  1085. expected_status=status.HTTP_200_OK)
  1086. def test_can_download_labeling_csv(self):
  1087. self.download_test_helper(url=self.labeling_url,
  1088. format='csv',
  1089. expected_status=status.HTTP_200_OK)
  1090. def test_can_download_seq2seq_csv(self):
  1091. self.download_test_helper(url=self.seq2seq_url,
  1092. format='csv',
  1093. expected_status=status.HTTP_200_OK)
  1094. def test_can_download_classification_jsonl(self):
  1095. self.download_test_helper(url=self.classification_url,
  1096. format='json',
  1097. expected_status=status.HTTP_200_OK)
  1098. def test_can_download_labeling_jsonl(self):
  1099. self.download_test_helper(url=self.labeling_url,
  1100. format='json',
  1101. expected_status=status.HTTP_200_OK)
  1102. def test_can_download_seq2seq_jsonl(self):
  1103. self.download_test_helper(url=self.seq2seq_url,
  1104. format='json',
  1105. expected_status=status.HTTP_200_OK)
  1106. def test_can_download_labelling_json1(self):
  1107. self.download_test_helper(url=self.labeling_url,
  1108. format='json1',
  1109. expected_status=status.HTTP_200_OK)
  1110. def test_can_download_plain_text(self):
  1111. self.download_test_helper(url=self.classification_url,
  1112. format='plain',
  1113. expected_status=status.HTTP_400_BAD_REQUEST)
  1114. class TestStatisticsAPI(APITestCase):
  1115. @classmethod
  1116. def setUpTestData(cls):
  1117. cls.super_user_name = 'super_user_name'
  1118. cls.super_user_pass = 'super_user_pass'
  1119. create_default_roles()
  1120. # Todo: change super_user to project_admin.
  1121. super_user = User.objects.create_superuser(username=cls.super_user_name,
  1122. password=cls.super_user_pass,
  1123. email='fizz@buzz.com')
  1124. main_project = mommy.make('TextClassificationProject', users=[super_user])
  1125. doc1 = mommy.make('Document', project=main_project)
  1126. mommy.make('Document', project=main_project)
  1127. mommy.make('DocumentAnnotation', document=doc1, user=super_user)
  1128. cls.url = reverse(viewname='statistics', args=[main_project.id])
  1129. cls.doc = Document.objects.filter(project=main_project)
  1130. def test_returns_exact_progress(self):
  1131. self.client.login(username=self.super_user_name,
  1132. password=self.super_user_pass)
  1133. response = self.client.get(self.url, format='json')
  1134. self.assertEqual(response.data['total'], 2)
  1135. self.assertEqual(response.data['remaining'], 1)
  1136. def test_returns_user_count(self):
  1137. self.client.login(username=self.super_user_name,
  1138. password=self.super_user_pass)
  1139. response = self.client.get(self.url, format='json')
  1140. self.assertIn('label', response.data)
  1141. self.assertIsInstance(response.data['label'], dict)
  1142. def test_returns_label_count(self):
  1143. self.client.login(username=self.super_user_name,
  1144. password=self.super_user_pass)
  1145. response = self.client.get(self.url, format='json')
  1146. self.assertIn('user', response.data)
  1147. self.assertIsInstance(response.data['user'], dict)
  1148. def test_returns_partial_response(self):
  1149. self.client.login(username=self.super_user_name,
  1150. password=self.super_user_pass)
  1151. response = self.client.get(f'{self.url}?include=user', format='json')
  1152. self.assertEqual(list(response.data.keys()), ['user'])
  1153. class TestUserAPI(APITestCase):
  1154. @classmethod
  1155. def setUpTestData(cls):
  1156. cls.super_user_name = 'super_user_name'
  1157. cls.super_user_pass = 'super_user_pass'
  1158. create_default_roles()
  1159. User.objects.create_superuser(username=cls.super_user_name,
  1160. password=cls.super_user_pass,
  1161. email='fizz@buzz.com')
  1162. cls.url = reverse(viewname='user_list')
  1163. def test_returns_user_count(self):
  1164. self.client.login(username=self.super_user_name,
  1165. password=self.super_user_pass)
  1166. response = self.client.get(self.url, format='json')
  1167. self.assertEqual(1, len(response.data))
  1168. class TestRoleAPI(APITestCase):
  1169. @classmethod
  1170. def setUpTestData(cls):
  1171. cls.user_name = 'user_name'
  1172. cls.user_pass = 'user_pass'
  1173. cls.project_admin_name = 'project_admin_name'
  1174. cls.project_admin_pass = 'project_admin_pass'
  1175. create_default_roles()
  1176. cls.user = User.objects.create_user(username=cls.user_name,
  1177. password=cls.user_pass)
  1178. User.objects.create_superuser(username=cls.project_admin_name,
  1179. password=cls.project_admin_pass,
  1180. email='fizz@buzz.com')
  1181. cls.url = reverse(viewname='roles')
  1182. def test_cannot_create_multiple_roles_with_same_name(self):
  1183. self.client.login(username=self.project_admin_name,
  1184. password=self.project_admin_pass)
  1185. roles = [
  1186. {'name': 'examplerole', 'description': 'example'},
  1187. {'name': 'examplerole', 'description': 'example'}
  1188. ]
  1189. self.client.post(self.url, format='json', data=roles[0])
  1190. second_response = self.client.post(self.url, format='json', data=roles[1])
  1191. self.assertEqual(second_response.status_code, status.HTTP_400_BAD_REQUEST)
  1192. def test_nonadmin_cannot_create_role(self):
  1193. self.client.login(username=self.user_name,
  1194. password=self.user_pass)
  1195. data = {'name': 'testrole', 'description': 'example'}
  1196. response = self.client.post(self.url, format='json', data=data)
  1197. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  1198. def test_admin_can_create_role(self):
  1199. self.client.login(username=self.project_admin_name,
  1200. password=self.project_admin_pass)
  1201. data = {'name': 'testrole', 'description': 'example'}
  1202. response = self.client.post(self.url, format='json', data=data)
  1203. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  1204. def test_admin_can_get_roles(self):
  1205. self.client.login(username=self.project_admin_name,
  1206. password=self.project_admin_pass)
  1207. response = self.client.get(self.url, format='json')
  1208. self.assertEqual(response.status_code, status.HTTP_200_OK)
  1209. class TestRoleMappingListAPI(APITestCase):
  1210. @classmethod
  1211. def setUpTestData(cls):
  1212. cls.project_member_name = 'project_member_name'
  1213. cls.project_member_pass = 'project_member_pass'
  1214. cls.second_project_member_name = 'second_project_member_name'
  1215. cls.second_project_member_pass = 'second_project_member_pass'
  1216. cls.project_admin_name = 'project_admin_name'
  1217. cls.project_admin_pass = 'project_admin_pass'
  1218. create_default_roles()
  1219. project_member = User.objects.create_user(username=cls.project_member_name,
  1220. password=cls.project_member_pass)
  1221. cls.second_project_member = User.objects.create_user(username=cls.second_project_member_name,
  1222. password=cls.second_project_member_pass)
  1223. project_admin = User.objects.create_user(username=cls.project_admin_name,
  1224. password=cls.project_admin_pass)
  1225. cls.main_project = mommy.make('Project', users=[project_member, project_admin, cls.second_project_member])
  1226. cls.other_project = mommy.make('Project', users=[cls.second_project_member, project_admin])
  1227. cls.admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
  1228. cls.role = mommy.make('Role', name='otherrole')
  1229. mommy.make('RoleMapping', role=cls.admin_role, project=cls.main_project, user=project_admin)
  1230. cls.data = {'user': project_member.id, 'role': cls.admin_role.id, 'project': cls.main_project.id}
  1231. cls.other_url = reverse(viewname='rolemapping_list', args=[cls.other_project.id])
  1232. cls.url = reverse(viewname='rolemapping_list', args=[cls.main_project.id])
  1233. def test_returns_mappings_to_project_admin(self):
  1234. self.client.login(username=self.project_admin_name,
  1235. password=self.project_admin_pass)
  1236. response = self.client.get(self.url, format='json')
  1237. self.assertEqual(response.status_code, status.HTTP_200_OK)
  1238. def test_allows_superuser_to_create_mapping(self):
  1239. self.client.login(username=self.project_admin_name,
  1240. password=self.project_admin_pass)
  1241. response = self.client.post(self.url, format='json', data=self.data)
  1242. self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  1243. def test_do_not_allow_nonadmin_to_create_mapping(self):
  1244. self.client.login(username=self.project_member_name,
  1245. password=self.project_member_pass)
  1246. response = self.client.post(self.url, format='json', data=self.data)
  1247. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  1248. def test_do_not_return_mappings_to_nonadmin(self):
  1249. self.client.login(username=self.project_member_name,
  1250. password=self.project_member_pass)
  1251. response = self.client.get(self.url, format='json')
  1252. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  1253. class TestRoleMappingDetailAPI(APITestCase):
  1254. @classmethod
  1255. def setUpTestData(cls):
  1256. cls.project_admin_name = 'project_admin_name'
  1257. cls.project_admin_pass = 'project_admin_pass'
  1258. cls.project_member_name = 'project_member_name'
  1259. cls.project_member_pass = 'project_member_pass'
  1260. cls.non_project_member_name = 'non_project_member_name'
  1261. cls.non_project_member_pass = 'non_project_member_pass'
  1262. create_default_roles()
  1263. project_admin = User.objects.create_user(username=cls.project_admin_name,
  1264. password=cls.project_admin_pass)
  1265. project_member = User.objects.create_user(username=cls.project_member_name,
  1266. password=cls.project_member_pass)
  1267. User.objects.create_user(username=cls.non_project_member_name, password=cls.non_project_member_pass)
  1268. project = mommy.make('Project', users=[project_admin, project_member])
  1269. admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
  1270. annotator_role = Role.objects.get(name=settings.ROLE_ANNOTATOR)
  1271. cls.rolemapping = mommy.make('RoleMapping', role=admin_role, project=project, user=project_admin)
  1272. cls.url = reverse(viewname='rolemapping_detail', args=[project.id, cls.rolemapping.id])
  1273. cls.data = {'role': annotator_role.id }
  1274. def test_returns_rolemapping_to_project_member(self):
  1275. self.client.login(username=self.project_admin_name,
  1276. password=self.project_admin_pass)
  1277. response = self.client.get(self.url, format='json')
  1278. self.assertEqual(response.data['id'], self.rolemapping.id)
  1279. def test_do_not_return_mapping_to_non_project_member(self):
  1280. self.client.login(username=self.non_project_member_name,
  1281. password=self.non_project_member_pass)
  1282. response = self.client.get(self.url, format='json')
  1283. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  1284. def test_allows_admin_to_update_mapping(self):
  1285. self.client.login(username=self.project_admin_name,
  1286. password=self.project_admin_pass)
  1287. response = self.client.patch(self.url, format='json', data=self.data)
  1288. self.assertEqual(response.data['role'], self.data['role'])
  1289. def test_disallows_project_member_to_update_mapping(self):
  1290. self.client.login(username=self.project_member_name,
  1291. password=self.project_member_pass)
  1292. response = self.client.patch(self.url, format='json', data=self.data)
  1293. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  1294. def test_allows_admin_to_delete_mapping(self):
  1295. self.client.login(username=self.project_admin_name,
  1296. password=self.project_admin_pass)
  1297. response = self.client.delete(self.url, format='json')
  1298. self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
  1299. def test_disallows_project_member_to_delete_mapping(self):
  1300. self.client.login(username=self.project_member_name,
  1301. password=self.project_member_pass)
  1302. response = self.client.delete(self.url, format='json')
  1303. self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)