diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..c3ba895a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - "3.6" +install: + - pip install -r requirements.txt +script: + - python app/manage.py migrate + - python app/manage.py collectstatic + - python app/manage.py test server.tests diff --git a/README.md b/README.md index a157a8c7..45f229ec 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # doccano +[![Build Status](https://travis-ci.org/chakki-works/doccano.svg?branch=master)](https://travis-ci.org/chakki-works/doccano) + doccano is an open source text annotation tool for human. It provides annotation features for text classification, sequence labeling and sequence to sequence. So, you can create labeled data for sentiment analysis, named entity recognition, text summarization and so on. Just create project, upload data and start annotation. You can build dataset in hours. ## Demo diff --git a/app/app/settings.py b/app/app/settings.py index dfea879d..f1e19f71 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -163,4 +163,4 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # on the import phase IMPORT_BATCH_SIZE = 500 -django_heroku.settings(locals()) +django_heroku.settings(locals(), test_runner=False) diff --git a/app/server/tests/test_annotation_api.py b/app/server/tests/test_annotation_api.py index 2e25b7c4..d3ae11dd 100644 --- a/app/server/tests/test_annotation_api.py +++ b/app/server/tests/test_annotation_api.py @@ -6,120 +6,68 @@ from ..models import * class TestAnnotationAPI(APITestCase): - def setUp(self): - self.username, self.password = 'user', 'pass' - - def create_user(self): - user = User.objects.create_user(username=self.username, password=self.password) - - return user - - def create_superuser(self): - user = User.objects.create_superuser(username=self.username, - password=self.password, - email='hoge@example.com') - return user - - def create_project(self): - project = mixer.blend('server.Project') - - return project - def create_label(self): - label = mixer.blend('server.Label') + @classmethod + def setUpTestData(cls): + cls.username = 'user' + cls.password = 'pass' + cls.user1 = User.objects.create_user(username=cls.username, password=cls.password) + cls.user2 = User.objects.create_user(username='user2', password='pass2') + cls.project1 = mixer.blend('server.Project', project_type=Project.DOCUMENT_CLASSIFICATION, + users=[cls.user1, cls.user2]) + cls.project2 = mixer.blend('server.Project', project_type=Project.DOCUMENT_CLASSIFICATION, + users=[cls.user2]) + cls.doc1 = mixer.blend('server.Document', project=cls.project1) + cls.doc2 = mixer.blend('server.Document', project=cls.project1) + cls.label = mixer.blend('server.Label', project=cls.project1) + cls.annotation1 = mixer.blend('server.DocumentAnnotation', document=cls.doc1, user=cls.user1) + cls.annotation2 = mixer.blend('server.DocumentAnnotation', document=cls.doc1, user=cls.user2) - return label - - def create_doc(self): - doc = mixer.blend('server.Document') - - return doc - - def create_annotation(self): - annotation = mixer.blend('server.DocumentAnnotation') - - return annotation + def setUp(self): + self.client.login(username=self.username, password=self.password) - def test_get_own_annotation(self): + def test_fetch_own_annotation(self): """ - Ensure we can get own annotation objects. + Ensure user can fetch only own annotation. """ - user = self.create_user() - project = self.create_project() - project.project_type = Project.DOCUMENT_CLASSIFICATION - annotation = self.create_annotation() - annotation.user = user - project.users.add(user) - project.documents.add(annotation.document) - project.save() - annotation.save() - url = reverse('annotations', args=[project.id, annotation.document.id]) + url = reverse('annotations', args=[self.project1.id, self.doc1.id]) + r = self.client.get(url, format='json') + self.assertEqual(r.status_code, status.HTTP_200_OK) + self.assertEqual(len(r.data), 1) + self.assertEqual(r.data[0]['id'], self.annotation1.id) - self.client.login(username=self.username, password=self.password) - response = self.client.get(url, format='json') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIsInstance(response.data, list) - self.assertEqual(response.data[0]['id'], annotation.id) - - def test_get_others_annotation(self): + def test_fetch_other_projects_annotation(self): """ - Ensure we cannot get others annotation objects. + Ensure user cannot fetch other project's annotation. """ - user = self.create_user() - project = self.create_project() - project.project_type = Project.DOCUMENT_CLASSIFICATION - annotation = self.create_annotation() - project.users.add(annotation.user) - project.documents.add(annotation.document) - project.save() - annotation.save() - url = reverse('annotations', args=[project.id, annotation.document.id]) - - self.client.login(username=self.username, password=self.password) - response = self.client.get(url, format='json') - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + url = reverse('annotations', args=[self.project2.id, self.doc1.id]) + r = self.client.get(url, format='json') + self.assertEqual(r.status_code, status.HTTP_403_FORBIDDEN) - def test_create_annotation(self): + def test_annotate_doc(self): """ - Ensure we can create a new annotation object. + Ensure user can annotate a document. """ - user = self.create_user() - project = self.create_project() - doc = self.create_doc() - label = self.create_label() - project.project_type = Project.DOCUMENT_CLASSIFICATION - project.users.add(user) - project.documents.add(doc) - project.labels.add(label) - - data = {'label_id': label.id} - url = reverse('annotations', args=[project.id, doc.id]) - self.client.login(username=self.username, password=self.password) - response = self.client.post(url, data, format='json') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(DocumentAnnotation.objects.count(), 1) + # Try to annotate a empty document(doc2). + data = {'label': self.label.id} + url = reverse('annotations', args=[self.project1.id, self.doc2.id]) + r = self.client.post(url, data, format='json') + self.assertEqual(r.status_code, status.HTTP_201_CREATED) + self.assertEqual(len(self.doc2.doc_annotations.all()), 1) def test_delete_annotation(self): """ - Ensure we cannot create a new project object by user. + Ensure user can delete only own annotation. """ - user = self.create_user() - project = self.create_project() - project.project_type = Project.DOCUMENT_CLASSIFICATION - annotation = self.create_annotation() - annotation.user = user - project.users.add(annotation.user) - project.documents.add(annotation.document) - project.save() - annotation.save() - - url = reverse('ann', args=[project.id, annotation.document.id, annotation.id]) - self.assertEqual(DocumentAnnotation.objects.count(), 1) - self.client.login(username=self.username, password=self.password) - response = self.client.delete(url, format='json') - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(DocumentAnnotation.objects.count(), 0) + self.assertEqual(len(self.doc1.doc_annotations.all()), 2) + + # Try to delete own annotation. + url = reverse('ann', args=[self.project1.id, self.doc1.id, self.annotation1.id]) + r = self.client.delete(url, format='json') + self.assertEqual(r.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(len(self.doc1.doc_annotations.all()), 1) + + # Try to delete other's annotation. + url = reverse('ann', args=[self.project1.id, self.doc1.id, self.annotation2.id]) + r = self.client.delete(url, format='json') + self.assertEqual(r.status_code, status.HTTP_403_FORBIDDEN) diff --git a/app/server/tests/test_models.py b/app/server/tests/test_models.py index 8efce6dc..3ca4f93d 100644 --- a/app/server/tests/test_models.py +++ b/app/server/tests/test_models.py @@ -2,6 +2,7 @@ from django.test import TestCase from django.core.exceptions import ValidationError from django.db.utils import IntegrityError from mixer.backend.django import mixer +from server.models import Label, DocumentAnnotation, SequenceAnnotation, Seq2seqAnnotation class TestProject(TestCase): @@ -12,7 +13,7 @@ class TestProject(TestCase): def test_get_progress(self): project = mixer.blend('server.Project') - res = project.get_progress() + res = project.get_progress(None) self.assertEqual(res['total'], 0) self.assertEqual(res['remaining'], 0) @@ -23,41 +24,33 @@ class TestLabel(TestCase): label = mixer.blend('server.Label') mixer.blend('server.Label', shortcut=label.shortcut) with self.assertRaises(IntegrityError): - mixer.blend('server.Label', - project=label.project, - shortcut=label.shortcut) + Label(project=label.project, shortcut=label.shortcut).save() def test_text_uniqueness(self): label = mixer.blend('server.Label') mixer.blend('server.Label', text=label.text) with self.assertRaises(IntegrityError): - mixer.blend('server.Label', - project=label.project, - text=label.text) + Label(project=label.project, text=label.text).save() class TestDocumentAnnotation(TestCase): def test_uniqueness(self): - annotation1 = mixer.blend('server.DocumentAnnotation') + a = mixer.blend('server.DocumentAnnotation') with self.assertRaises(IntegrityError): - mixer.blend('server.DocumentAnnotation', - document=annotation1.document, - user=annotation1.user, - label=annotation1.label) + DocumentAnnotation(document=a.document, user=a.user, label=a.label).save() class TestSequenceAnnotation(TestCase): def test_uniqueness(self): - annotation1 = mixer.blend('server.SequenceAnnotation') + a = mixer.blend('server.SequenceAnnotation') with self.assertRaises(IntegrityError): - mixer.blend('server.SequenceAnnotation', - document=annotation1.document, - user=annotation1.user, - label=annotation1.label, - start_offset=annotation1.start_offset, - end_offset=annotation1.end_offset) + SequenceAnnotation(document=a.document, + user=a.user, + label=a.label, + start_offset=a.start_offset, + end_offset=a.end_offset).save() def test_position_constraint(self): with self.assertRaises(ValidationError): @@ -68,9 +61,8 @@ class TestSequenceAnnotation(TestCase): class TestSeq2seqAnnotation(TestCase): def test_uniqueness(self): - annotation1 = mixer.blend('server.Seq2seqAnnotation') + a = mixer.blend('server.Seq2seqAnnotation') with self.assertRaises(IntegrityError): - mixer.blend('server.Seq2seqAnnotation', - document=annotation1.document, - user=annotation1.user, - text=annotation1.text) + Seq2seqAnnotation(document=a.document, + user=a.user, + text=a.text).save() diff --git a/app/server/tests/test_projects_api.py b/app/server/tests/test_projects_api.py index 28968998..0ce61653 100644 --- a/app/server/tests/test_projects_api.py +++ b/app/server/tests/test_projects_api.py @@ -7,59 +7,37 @@ from ..models import * class TestProjects(APITestCase): - def setUp(self): - self.username, self.password = 'user', 'pass' - self.url = reverse('project-list') - - def create_user(self): - user = User.objects.create_user(username=self.username, password=self.password) - - return user - - def create_superuser(self): - user = User.objects.create_superuser(username=self.username, - password=self.password, - email='hoge@example.com') - return user + @classmethod + def setUpTestData(cls): + cls.username = 'user' + cls.password = 'pass' + cls.super_username = 'super' + cls.normal_user = User.objects.create_user(username=cls.username, password=cls.password) + cls.super_user = User.objects.create_superuser(username=cls.super_username, + password=cls.password, email='fizz@buzz.com') + cls.project1 = mixer.blend('server.Project', project_type=Project.DOCUMENT_CLASSIFICATION, + users=[cls.normal_user, cls.super_user]) + cls.project2 = mixer.blend('server.Project', project_type=Project.DOCUMENT_CLASSIFICATION, + users=[cls.super_user]) + cls.url = reverse('project-list') - def create_project(self): - project = mixer.blend('server.Project') - - return project - - def add_user_to_project(self, user, project): - project.users.add(user) + def setUp(self): + self.client.login(username=self.username, password=self.password) def test_get_projects(self): """ - Ensure we can get project objects. + Ensure user can get project. """ - user = self.create_user() - project = self.create_project() - self.add_user_to_project(user, project) - - self.client.login(username=self.username, password=self.password) response = self.client.get(self.url, format='json') - p = response.data[0] - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIsInstance(response.data, list) - self.assertEqual(p['id'], project.id) - self.assertEqual(p['name'], project.name) - self.assertEqual(p['description'], project.description) - self.assertEqual(p['project_type'], project.project_type) - self.assertEqual(p['image'], project.image) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['id'], self.project1.id) def test_get_progress(self): """ - Ensure we can get project's progress. + Ensure user can get project's progress. """ - user = self.create_user() - project = self.create_project() - self.add_user_to_project(user, project) - - url = '{}{}/progress/'.format(self.url, project.id) - self.client.login(username=self.username, password=self.password) + url = '{}{}/progress/'.format(self.url, self.project1.id) response = self.client.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -68,60 +46,23 @@ class TestProjects(APITestCase): self.assertIsInstance(response.data['total'], int) self.assertIsInstance(response.data['remaining'], int) - def test_create_project_by_admin(self): - """ - Ensure we can create a new project object by admin. - """ - user = self.create_superuser() - data = {'name': 'DabApps', - 'description': 'desc', - 'project_type': Project.DOCUMENT_CLASSIFICATION, - 'users': [user.id]} - self.client.login(username=self.username, password=self.password) - response = self.client.post(self.url, data, format='json') - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Project.objects.count(), 1) - self.assertEqual(Project.objects.get().name, 'DabApps') - - def test_create_project_by_user(self): - """ - Ensure we cannot create a new project object by user. - """ - user = self.create_user() - data = {'name': 'DabApps', - 'description': 'desc', - 'project_type': Project.DOCUMENT_CLASSIFICATION, - 'users': [user.id]} - self.client.login(username=self.username, password=self.password) - response = self.client.post(self.url, data, format='json') - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_delete_project_by_superuser(self): + def test_superuser_can_delete_project(self): """ - Ensure we can delete a project by superuser. + Ensure superuser can delete a project. """ - user = self.create_superuser() - project = self.create_project() - self.assertEqual(Project.objects.count(), 1) - self.client.login(username=self.username, password=self.password) - url = '{}{}/'.format(self.url, project.id) + self.assertEqual(Project.objects.count(), 2) + self.client.login(username=self.super_username, password=self.password) + url = '{}{}/'.format(self.url, self.project2.id) response = self.client.delete(url, format='json') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Project.objects.count(), 0) + self.assertEqual(Project.objects.count(), 1) - def test_delete_project_by_user(self): + def test_normal_user_cannot_delete_project(self): """ - Ensure we cannot delete a project by user. + Ensure normal user cannot delete a project. """ - user = self.create_user() - project = self.create_project() - self.assertEqual(Project.objects.count(), 1) - self.client.login(username=self.username, password=self.password) - url = '{}{}/'.format(self.url, project.id) + self.assertEqual(Project.objects.count(), 2) + url = '{}{}/'.format(self.url, self.project2.id) response = self.client.delete(url, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) + self.assertEqual(Project.objects.count(), 2)