Browse Source

Merge branch 'master' into v1.3.0

pull/1310/head
Hiroki Nakayama 3 years ago
committed by GitHub
parent
commit
ffce5833f1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 326 additions and 11 deletions
  1. 10
      app/api/admin.py
  2. 22
      app/api/migrations/0009_tag.py
  3. 8
      app/api/models.py
  4. 15
      app/api/serializers.py
  5. 53
      app/api/tests/test_api.py
  6. 6
      app/api/urls.py
  7. 1
      app/api/views/__init__.py
  8. 24
      app/api/views/tag.py
  9. 40
      frontend/components/project/FormUpdate.vue
  10. 10
      frontend/components/project/ProjectList.vue
  11. 13
      frontend/domain/models/project/project.ts
  12. 46
      frontend/domain/models/tag/tag.ts
  13. 9
      frontend/domain/models/tag/tagRepository.ts
  14. 6
      frontend/plugins/services.ts
  15. 34
      frontend/repositories/tag/apiTagRepository.ts
  16. 4
      frontend/services/application/project/projectData.ts
  17. 23
      frontend/services/application/tag/tagApplicationService.ts
  18. 13
      frontend/services/application/tag/tagData.ts

10
app/api/admin.py

@ -2,7 +2,7 @@ from django.contrib import admin
from .models import (Comment, Document, DocumentAnnotation, Label, Project,
Role, RoleMapping, Seq2seqAnnotation, Seq2seqProject,
SequenceAnnotation, SequenceLabelingProject,
SequenceAnnotation, SequenceLabelingProject, Tag,
TextClassificationProject)
@ -53,6 +53,13 @@ class RoleMappingAdmin(admin.ModelAdmin):
ordering = ('user',)
search_fields = ('user__username',)
class TagAdmin(admin.ModelAdmin):
list_display = ('project', 'text', )
ordering = ('project', 'text', )
search_fields = ('text',)
class CommentAdmin(admin.ModelAdmin):
list_display = ('user', 'document', 'text', 'created_at', )
ordering = ('user', 'created_at', )
@ -71,3 +78,4 @@ admin.site.register(Seq2seqProject, ProjectAdmin)
admin.site.register(Role, RoleAdmin)
admin.site.register(RoleMapping, RoleMappingAdmin)
admin.site.register(Comment, CommentAdmin)
admin.site.register(Tag, TagAdmin)

22
app/api/migrations/0009_tag.py

@ -0,0 +1,22 @@
# Generated by Django 3.2 on 2021-04-13 16:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('api', '0008_auto_20210302_1013'),
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='api.project')),
],
),
]

8
app/api/models.py

@ -231,6 +231,14 @@ class Comment(models.Model):
ordering = ('-created_at', )
class Tag(models.Model):
text = models.TextField()
project = models.ForeignKey(Project, related_name='tags', on_delete=models.CASCADE)
def __str__(self):
return self.text
class Annotation(models.Model):
objects = AnnotationManager()

15
app/api/serializers.py

@ -10,7 +10,7 @@ from .models import (AutoLabelingConfig, Comment, Document, DocumentAnnotation,
Label, Project, Role, RoleMapping, Seq2seqAnnotation,
Seq2seqProject, SequenceAnnotation,
SequenceLabelingProject, Speech2textAnnotation,
Speech2textProject, TextClassificationProject)
Speech2textProject, Tag, TextClassificationProject)
class UserSerializer(serializers.ModelSerializer):
@ -69,6 +69,14 @@ class CommentSerializer(serializers.ModelSerializer):
read_only_fields = ('user', 'document')
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('id', 'project', 'text', )
read_only_fields = ('id', 'project')
class DocumentSerializer(serializers.ModelSerializer):
annotations = serializers.SerializerMethodField()
annotation_approver = serializers.SerializerMethodField()
@ -103,6 +111,7 @@ class ApproverSerializer(DocumentSerializer):
class ProjectSerializer(serializers.ModelSerializer):
current_users_role = serializers.SerializerMethodField()
tags = TagSerializer(many=True, required=False)
def get_current_users_role(self, instance):
role_abstractor = {
@ -122,8 +131,8 @@ class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('id', 'name', 'description', 'guideline', 'users', 'current_users_role', 'project_type',
'updated_at', 'randomize_document_order', 'collaborative_annotation', 'single_class_classification')
read_only_fields = ('updated_at', 'users', 'current_users_role')
'updated_at', 'randomize_document_order', 'collaborative_annotation', 'single_class_classification', 'tags')
read_only_fields = ('updated_at', 'users', 'current_users_role', 'tags')
class TextClassificationProjectSerializer(ProjectSerializer):

53
app/api/tests/test_api.py

@ -833,6 +833,59 @@ class TestCommentListAPI(APITestCase):
remove_all_role_mappings()
class TestTagAPI(APITestCase):
@classmethod
def setUpTestData(cls):
cls.main_project_member_name = 'project_member_name'
cls.main_project_member_pass = 'project_member_pass'
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
create_default_roles()
main_project_member = User.objects.create_user(username=cls.main_project_member_name,
password=cls.main_project_member_pass)
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
cls.main_project = mommy.make('TextClassificationProject', users=[main_project_member, super_user])
assign_user_to_role(project_member=main_project_member, project=cls.main_project,
role_name=settings.ROLE_ANNOTATOR)
assign_user_to_role(project_member=super_user, project=cls.main_project,
role_name=settings.ROLE_ANNOTATOR)
cls.tag = mommy.make('Tag', project=cls.main_project, text='Tag 1')
cls.url = reverse(viewname='tag_list', args=[cls.main_project.id])
cls.project_url = reverse(viewname='project_list')
cls.delete_url = reverse(viewname='tag_list', args=[cls.main_project.id])
def test_create_tag(self):
self.client.login(username=self.super_user_name,
password=self.super_user_pass)
response = self.client.post(self.url, data={'text': 'Tag 2'})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get(self.project_url, format='json')
self.assertTrue(response.data[0]['tags'][1]['text'] == 'Tag 2' ,'Content of tags differs.')
def test_tag_list(self):
self.client.login(username=self.main_project_member_name,
password=self.main_project_member_pass)
response = self.client.get(self.project_url, format='json')
self.assertTrue(len(response.data[0]['tags']) == 1 ,'Number of tags differs expected amount.')
self.assertTrue(response.data[0]['tags'][0]['text'] == 'Tag 1' ,'Content of tags differs.')
def test_delete_tag(self):
self.client.login(username=self.super_user_name,
password=self.super_user_pass)
response = self.client.delete(self.delete_url, data={'id': self.tag.id})
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
response = self.client.get(self.project_url, format='json')
self.assertTrue(len(response.data[0]['tags']) == 0 ,'Number of tags differs expected amount.')
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestAnnotationListAPI(APITestCase, TestUtilsMixin):
@classmethod

6
app/api/urls.py

@ -67,6 +67,11 @@ urlpatterns_project = [
view=views.AnnotationDetail.as_view(),
name='annotation_detail'
),
path(
route='tags',
view=views.TagList.as_view(),
name='tag_list'
),
path(
route='docs/<int:doc_id>/comments',
view=views.CommentListDoc.as_view(),
@ -122,7 +127,6 @@ urlpatterns_project = [
view=views.AutoLabelingAnnotation.as_view(),
name='auto_labeling_annotation'
),
path(
route='auto-labeling-template-testing',
view=views.AutoLabelingTemplateTest.as_view(),

1
app/api/views/__init__.py

@ -10,5 +10,6 @@ from .label import *
from .project import *
from .role import *
from .statistics import *
from .tag import *
from .task import *
from .user import *

24
app/api/views/tag.py

@ -0,0 +1,24 @@
from django.shortcuts import get_object_or_404
from rest_framework import generics, status
from rest_framework.response import Response
from ..models import Project, Tag
from ..serializers import TagSerializer
class TagList(generics.ListCreateAPIView):
serializer_class = TagSerializer
pagination_class = None
def get_queryset(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
return project.tags
def perform_create(self, serializer):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
serializer.save(project=project)
def delete(self, request, *args, **kwargs):
delete_id = request.data['id']
Tag.objects.get(id=delete_id).delete()
return Response(status=status.HTTP_204_NO_CONTENT)

40
frontend/components/project/FormUpdate.vue

@ -99,6 +99,28 @@
</v-btn>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
sm="6"
>
<h3>Tags</h3>
<v-chip
v-for="tag in tags"
:key="tag.id"
close
outlined
@click:close="removeTag(tag.id)">{{tag.text}}
</v-chip>
<v-text-field
v-model="tagInput"
clearable
prepend-icon="add_circle"
v-on:keyup.enter="addTag()"
@click:prepend="addTag()">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
@ -134,12 +156,15 @@ import { projectNameRules, descriptionRules } from '@/rules/index'
export default {
async fetch() {
this.project = await this.$services.project.findById(this.projectId)
this.getTags()
},
data() {
return {
project: {},
tags: {},
beforeEditCache: {},
tagInput: '',
edit: {
name: false,
desc: false
@ -200,6 +225,21 @@ export default {
validate() {
return this.$refs.form.validate()
},
async getTags(){
this.tags = await this.$services.tag.list(this.projectId)
},
addTag(){
this.$services.tag.create(this.projectId, this.tagInput)
this.tagInput = ''
this.getTags()
},
removeTag(id){
this.$services.tag.delete(this.projectId, id)
this.tags = this.tags.filter(tag => tag.id !== id)
}
}
}

10
frontend/components/project/ProjectList.vue

@ -35,6 +35,13 @@
<template v-slot:[`item.updatedAt`]="{ item }">
<span>{{ item.updatedAt | dateParse('YYYY-MM-DDTHH:mm:ss') | dateFormat('DD/MM/YYYY HH:mm') }}</span>
</template>
<template v-slot:[`item.tags`]="{ item }">
<v-chip
v-for="tag in item.tags"
:key="tag.id"
outlined>{{tag.text}}
</v-chip>
</template>
</v-data-table>
</template>
@ -76,8 +83,9 @@ export default Vue.extend({
{ text: this.$t('generic.description'), value: 'description' },
{ text: this.$t('generic.type'), value: 'projectType' },
{ text: 'Updated', value: 'updatedAt' },
{ text: 'Tags', value: 'tags'}
]
}
}
}
})
</script>

13
frontend/domain/models/project/project.ts

@ -21,6 +21,7 @@ export class ProjectReadItem {
public collaborative_annotation: boolean,
public single_class_classification: boolean,
public resourcetype: string,
public tags: Object[],
) {}
static valueOf(
@ -36,7 +37,8 @@ export class ProjectReadItem {
randomize_document_order,
collaborative_annotation,
single_class_classification,
resourcetype
resourcetype,
tags
}:
{
id: number,
@ -50,7 +52,8 @@ export class ProjectReadItem {
randomize_document_order: boolean,
collaborative_annotation: boolean,
single_class_classification: boolean,
resourcetype: string
resourcetype: string,
tags: Object[]
}
): ProjectReadItem {
return new ProjectReadItem(
@ -65,7 +68,8 @@ export class ProjectReadItem {
randomize_document_order,
collaborative_annotation,
single_class_classification,
resourcetype
resourcetype,
tags
)
}
@ -109,7 +113,8 @@ export class ProjectReadItem {
randomize_document_order: this.randomize_document_order,
collaborative_annotation: this.collaborative_annotation,
single_class_classification: this.single_class_classification,
resourcetype: this.resourcetype
resourcetype: this.resourcetype,
tags: this.tags
}
}
}

46
frontend/domain/models/tag/tag.ts

@ -0,0 +1,46 @@
export class TagItemList {
constructor(public tagItems: TagItem[]) {}
static valueOf(items: TagItem[]): TagItemList {
return new TagItemList(items)
}
add(item: TagItem) {
this.tagItems.push(item)
}
delete(item: TagItem) {
this.tagItems = this.tagItems.filter(tag => tag.id !== item.id)
}
ids(): Number[]{
return this.tagItems.map(item => item.id)
}
toArray(): Object[] {
return this.tagItems.map(item => item.toObject())
}
}
export class TagItem {
constructor(
public id: number,
public text: string,
public project: string
) {}
static valueOf(
{ id, text, project }:
{ id: number, text: string, project: string }
): TagItem {
return new TagItem(id, text, project)
}
toObject(): Object {
return {
id: this.id,
text: this.text,
project: this.project
}
}
}

9
frontend/domain/models/tag/tagRepository.ts

@ -0,0 +1,9 @@
import { TagItem } from '~/domain/models/tag/tag'
export interface TagRepository {
list(projectId: string): Promise<TagItem[]>
create(projectId: string, item: string): Promise<TagItem>
delete(projectId: string, tagId: number): Promise<void>
}

6
frontend/plugins/services.ts

@ -39,6 +39,8 @@ import { APIDownloadFormatRepository } from '~/repositories/download/apiDownload
import { APIDownloadRepository } from '~/repositories/download/apiDownloadRepository'
import { DownloadApplicationService } from '~/services/application/download/downloadApplicationService'
import { DownloadFormatApplicationService } from '~/services/application/download/downloadFormatApplicationService'
import { APITagRepository } from '~/repositories/tag/apiTagRepository'
import { TagApplicationService } from '~/services/application/tag/tagApplicationService'
export interface Services {
label: LabelApplicationService,
@ -61,6 +63,7 @@ export interface Services {
taskStatus: TaskStatusApplicationService,
downloadFormat: DownloadFormatApplicationService,
download: DownloadApplicationService,
tag: TagApplicationService,
}
declare module 'vue/types/vue' {
@ -83,6 +86,7 @@ const plugin: Plugin = (context, inject) => {
const seq2seqRepository = new APISeq2seqRepository()
const optionRepository = new LocalStorageOptionRepository()
const configRepository = new APIConfigRepository()
const tagRepository = new APITagRepository()
const templateRepository = new APITemplateRepository()
const authRepository = new APIAuthRepository()
const catalogRepository = new APICatalogRepository()
@ -104,6 +108,7 @@ const plugin: Plugin = (context, inject) => {
const seq2seq = new Seq2seqApplicationService(seq2seqRepository)
const option = new OptionApplicationService(optionRepository)
const config = new ConfigApplicationService(configRepository)
const tag = new TagApplicationService(tagRepository)
const template = new TemplateApplicationService(templateRepository)
const auth = new AuthApplicationService(authRepository)
const catalog = new CatalogApplicationService(catalogRepository)
@ -133,6 +138,7 @@ const plugin: Plugin = (context, inject) => {
taskStatus,
downloadFormat,
download,
tag,
}
inject('services', services)
}

34
frontend/repositories/tag/apiTagRepository.ts

@ -0,0 +1,34 @@
import ApiService from '@/services/api.service'
import { TagRepository } from '~/domain/models/tag/tagRepository'
import { TagItem } from '~/domain/models/tag/tag'
export interface TagItemResponse {
id: number,
text: string,
project: string
}
export class APITagRepository implements TagRepository {
constructor(
private readonly request = ApiService
) {}
async list(projectId: string): Promise<TagItem[]> {
const url = `/projects/${projectId}/tags`
const response = await this.request.get(url)
const responseItems: TagItemResponse[] = response.data
return responseItems.map(item => TagItem.valueOf(item))
}
async create(projectId: string, item: string): Promise<TagItem> {
const url = `/projects/${projectId}/tags`
const response = await this.request.post(url, { text: item })
const responseItem: TagItemResponse = response.data
return TagItem.valueOf(responseItem)
}
async delete(projectId: string, tagId: number): Promise<void> {
const url = `/projects/${projectId}/tags`
await this.request.delete(url, { id: tagId })
}
}

4
frontend/services/application/project/projectData.ts

@ -14,6 +14,7 @@ export class ProjectDTO {
pageLink: string
permitApprove: Boolean
filterOption: String
tags: Object[]
constructor(item: ProjectReadItem) {
this.id = item.id
@ -29,7 +30,8 @@ export class ProjectDTO {
this.pageLink = item.annotationPageLink
this.permitApprove = item.permitApprove
this.filterOption = item.filterOption
this.tags = item.tags
}
}
export type ProjectWriteDTO = Pick<ProjectDTO, 'id' | 'name' | 'description' | 'guideline' | 'projectType' | 'enableRandomizeDocOrder' | 'enableShareAnnotation' | 'singleClassClassification'>
export type ProjectWriteDTO = Pick<ProjectDTO, 'id' | 'name' | 'description' | 'guideline' | 'projectType' | 'enableRandomizeDocOrder' | 'enableShareAnnotation' | 'singleClassClassification' | 'tags'>

23
frontend/services/application/tag/tagApplicationService.ts

@ -0,0 +1,23 @@
import { TagDTO } from './tagData'
import { TagRepository } from '~/domain/models/tag/tagRepository'
import { TagItem } from '~/domain/models/tag/tag'
export class TagApplicationService {
constructor(
private readonly repository: TagRepository
) {}
public async list(id: string): Promise<TagDTO[]> {
const items = await this.repository.list(id)
return items.map(item => new TagDTO(item))
}
public create(projectId: string, text: string): void {
this.repository.create(projectId, text)
}
public delete(projectId: string, id: number): Promise<void> {
return this.repository.delete(projectId, id)
}
}

13
frontend/services/application/tag/tagData.ts

@ -0,0 +1,13 @@
import { TagItem } from '~/domain/models/tag/tag'
export class TagDTO {
id: number
text: string
project: string
constructor(item: TagItem) {
this.id = item.id
this.text = item.text
this.project = item.project
}
}
Loading…
Cancel
Save