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.

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