Browse Source

Merge pull request #1585 from doccano/enhancement/speedUpFetchingComment

Speed up fetching comment
pull/1587/head
Hiroki Nakayama 3 years ago
committed by GitHub
parent
commit
5708606318
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 64 deletions
  1. 4
      backend/api/tests/api/test_comment.py
  2. 1
      backend/api/views/comment.py
  3. 58
      frontend/components/comment/CommentList.vue
  4. 53
      frontend/domain/models/comment/comment.ts
  5. 6
      frontend/domain/models/comment/commentRepository.ts
  6. 24
      frontend/pages/projects/_id/comments/index.vue
  7. 11
      frontend/repositories/comment/apiCommentRepository.ts
  8. 10
      frontend/services/application/comment/commentApplicationService.ts
  9. 16
      frontend/services/application/comment/commentData.ts

4
backend/api/tests/api/test_comment.py

@ -50,7 +50,7 @@ class TestCommentListProjectAPI(CRUDMixin):
def test_allows_project_member_to_list_comments(self):
for member in self.project.users:
response = self.assert_fetch(member, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data['count'], 1)
def test_denies_non_project_member_to_list_comments(self):
self.assert_fetch(self.non_member, status.HTTP_403_FORBIDDEN)
@ -70,7 +70,7 @@ class TestCommentListProjectAPI(CRUDMixin):
for member in self.project.users:
self.assert_bulk_delete(member, status.HTTP_204_NO_CONTENT)
response = self.client.get(self.url)
self.assertEqual(len(response.data), 0)
self.assertEqual(response.data['count'], 0)
def test_denies_non_project_member_to_delete_comments(self):
self.assert_fetch(self.non_member, status.HTTP_403_FORBIDDEN)

1
backend/api/views/comment.py

@ -26,7 +26,6 @@ class CommentListDoc(generics.ListCreateAPIView):
class CommentListProject(generics.ListAPIView):
pagination_class = None
permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
serializer_class = CommentSerializer
filter_backends = (DjangoFilterBackend, filters.SearchFilter)

58
frontend/components/comment/CommentList.vue

@ -3,13 +3,15 @@
:value="value"
:headers="headers"
:items="items"
:options.sync="options"
:server-items-length="total"
:search="search"
:loading="isLoading"
:loading-text="$t('generic.loading')"
:no-data-text="$t('vuetify.noDataAvailable')"
:footer-props="{
'showFirstLastPage': true,
'items-per-page-options': [5, 10, 15, 100],
'items-per-page-options': [10, 50, 100],
'items-per-page-text': $t('vuetify.itemsPerPageText'),
'page-text': $t('dataset.pageText')
}"
@ -30,6 +32,8 @@
filled
/>
</template>
<!--
Tempolary removing due to the performance
<template #[`item.action`]="{ item }">
<v-btn
small
@ -38,17 +42,17 @@
>
{{ $t('dataset.annotate') }}
</v-btn>
</template>
</template> -->
</v-data-table>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
import { mdiMagnify } from '@mdi/js'
import { DataOptions } from 'vuetify/types'
import VueFilterDateFormat from '@vuejs-community/vue-filter-date-format'
import VueFilterDateParse from '@vuejs-community/vue-filter-date-parse'
import { CommentReadDTO } from '~/services/application/comment/commentData'
import { ExampleDTO } from '~/services/application/example/exampleData'
Vue.use(VueFilterDateFormat)
Vue.use(VueFilterDateParse)
@ -59,11 +63,6 @@ export default Vue.extend({
default: false,
required: true
},
examples: {
type: Array as PropType<ExampleDTO[]>,
default: () => [],
required: true
},
items: {
type: Array as PropType<CommentReadDTO[]>,
default: () => [],
@ -73,12 +72,18 @@ export default Vue.extend({
type: Array as PropType<CommentReadDTO[]>,
default: () => [],
required: true
},
total: {
type: Number,
default: 0,
required: true
}
},
data() {
return {
search: '',
options: {} as DataOptions,
headers: [
{ text: this.$t('dataset.text'), value: 'text' },
{ text: this.$t('user.username'), value: 'username' },
@ -89,12 +94,37 @@ export default Vue.extend({
}
},
methods: {
toLabeling(item: CommentReadDTO) {
const index = this.examples.findIndex((example: ExampleDTO) => example.id === item.example)
const page = (index + 1).toString()
this.$emit('click:labeling', { page, q: this.search })
watch: {
options: {
handler() {
this.$emit('update:query', {
query: {
limit: this.options.itemsPerPage.toString(),
offset: ((this.options.page - 1) * this.options.itemsPerPage).toString(),
q: this.search
}
})
},
deep: true
},
search() {
this.$emit('update:query', {
query: {
limit: this.options.itemsPerPage.toString(),
offset: '0',
q: this.search
}
})
this.options.page = 1
}
}
},
// methods: {
// toLabeling(item: CommentReadDTO) {
// const index = this.examples.findIndex((example: ExampleDTO) => example.id === item.example)
// const page = (index + 1).toString()
// this.$emit('click:labeling', { page, q: this.search })
// }
// }
})
</script>

53
frontend/domain/models/comment/comment.ts

@ -1,38 +1,43 @@
export class CommentItemList {
constructor(public commentItems: CommentItem[]) {}
static valueOf(items: CommentItem[]): CommentItemList {
return new CommentItemList(items)
}
add(item: CommentItem) {
this.commentItems.push(item)
}
constructor(
private _count: number,
private _next: string | null,
private _prev: string | null,
private _items: CommentItem[]
) {}
update(item: CommentItem) {
const index = this.commentItems.findIndex(comment => comment.id === item.id)
this.commentItems.splice(index, 1, item)
static valueOf(
{ count, next, previous, results }:
{
count : number,
next : string | null,
previous: string | null,
results : Array<any>
}
delete(item: CommentItem) {
this.commentItems = this.commentItems.filter(comment => comment.id !== item.id)
): CommentItemList {
const items = results.map(item => CommentItem.valueOf(item))
return new CommentItemList(
count,
next,
previous,
items
)
}
deleteBulk(items: CommentItemList) {
const ids = items.ids()
this.commentItems = this.commentItems.filter(comment => !ids.includes(comment.id))
get count() {
return this._count
}
count(): Number {
return this.commentItems.length
get next() {
return this._next
}
ids(): Number[]{
return this.commentItems.map(item => item.id)
get prev() {
return this._prev
}
toArray(): Object[] {
return this.commentItems.map(item => item.toObject())
get items(): CommentItem[] {
return this._items
}
}

6
frontend/domain/models/comment/commentRepository.ts

@ -1,4 +1,4 @@
import { CommentItem } from '~/domain/models/comment/comment'
import { CommentItem, CommentItemList } from '~/domain/models/comment/comment'
export interface CommentItemResponse {
id: number,
@ -9,8 +9,10 @@ export interface CommentItemResponse {
created_at: string
}
export type SearchOption = {[key: string]: string | (string | null)[]}
export interface CommentRepository {
listAll(projectId: string, q: string): Promise<CommentItem[]>
listAll(projectId: string, { limit, offset, q }: SearchOption): Promise<CommentItemList>
list(projectId: string, docId: number): Promise<CommentItem[]>

24
frontend/pages/projects/_id/comments/index.vue

@ -19,9 +19,10 @@
</v-card-title>
<comment-list
v-model="selected"
:examples="examples.items"
:items="items"
:items="item.items"
:is-loading="isLoading"
:total="item.count"
@update:query="updateQuery"
@click:labeling="movePage"
/>
</v-card>
@ -29,9 +30,9 @@
<script lang="ts">
import Vue from 'vue'
import _ from 'lodash'
import CommentList from '@/components/comment/CommentList.vue'
import { CommentReadDTO } from '~/services/application/comment/commentData'
import { ExampleListDTO } from '~/services/application/example/exampleData'
import { CommentReadDTO, CommentListDTO } from '~/services/application/comment/commentData'
import { ProjectDTO } from '~/services/application/project/projectData'
import FormDelete from '~/components/comment/FormDelete.vue'
@ -51,9 +52,8 @@ export default Vue.extend({
return {
dialogDelete: false,
project: {} as ProjectDTO,
items: [] as CommentReadDTO[],
item: {} as CommentListDTO,
selected: [] as CommentReadDTO[],
examples: {} as ExampleListDTO,
isLoading: false
}
},
@ -61,9 +61,7 @@ export default Vue.extend({
async fetch() {
this.isLoading = true
this.project = await this.$services.project.findById(this.projectId)
this.items = await this.$services.comment.listProjectComment(this.projectId)
const example = await this.$services.example.fetchOne(this.projectId,'1','','') // to fetch the count of examples
this.examples = await this.$services.example.list(this.projectId, {limit: example.count.toString()})
this.item = await this.$services.comment.listProjectComment(this.projectId, this.$route.query)
this.isLoading = false
},
@ -76,6 +74,14 @@ export default Vue.extend({
}
},
watch: {
'$route.query': _.debounce(function() {
// @ts-ignore
this.$fetch()
}, 1000
),
},
methods: {
async remove() {
await this.$services.comment.deleteBulk(this.projectId, this.selected)

11
frontend/repositories/comment/apiCommentRepository.ts

@ -1,17 +1,16 @@
import ApiService from '@/services/api.service'
import { CommentRepository, CommentItemResponse } from '@/domain/models/comment/commentRepository'
import { CommentItem } from '~/domain/models/comment/comment'
import { CommentRepository, CommentItemResponse, SearchOption } from '@/domain/models/comment/commentRepository'
import { CommentItem, CommentItemList } from '~/domain/models/comment/comment'
export class APICommentRepository implements CommentRepository {
constructor(
private readonly request = ApiService
) {}
async listAll(projectId: string, q: string): Promise<CommentItem[]> {
const url = `/projects/${projectId}/comments?q=${q}`
async listAll(projectId: string, { limit = '10', offset = '0', q = '' }: SearchOption): Promise<CommentItemList> {
const url = `/projects/${projectId}/comments?q=${q}&limit=${limit}&offset=${offset}`
const response = await this.request.get(url)
const items: CommentItemResponse[] = response.data
return items.map(item => CommentItem.valueOf(item))
return CommentItemList.valueOf(response.data)
}
async list(projectId: string, exampleId: number): Promise<CommentItem[]> {

10
frontend/services/application/comment/commentApplicationService.ts

@ -1,5 +1,5 @@
import { CommentReadDTO } from './commentData'
import { CommentRepository } from '~/domain/models/comment/commentRepository'
import { CommentReadDTO, CommentListDTO } from './commentData'
import { CommentRepository, SearchOption } from '~/domain/models/comment/commentRepository'
import { CommentItem } from '~/domain/models/comment/comment'
export class CommentApplicationService {
@ -7,9 +7,9 @@ export class CommentApplicationService {
private readonly repository: CommentRepository
) {}
public async listProjectComment(projectId: string, q: string = ''): Promise<CommentReadDTO[]> {
const items = await this.repository.listAll(projectId, q)
return items.map(item => new CommentReadDTO(item))
public async listProjectComment(projectId: string, options: SearchOption): Promise<CommentListDTO> {
const item = await this.repository.listAll(projectId, options)
return new CommentListDTO(item)
}
public async list(projectId: string, docId: number): Promise<CommentReadDTO[]> {

16
frontend/services/application/comment/commentData.ts

@ -1,4 +1,4 @@
import { CommentItem } from '~/domain/models/comment/comment'
import { CommentItem, CommentItemList } from '~/domain/models/comment/comment'
export class CommentReadDTO {
@ -18,3 +18,17 @@ export class CommentReadDTO {
this.createdAt = item.createdAt;
}
}
export class CommentListDTO {
count: number
next : string | null
prev : string | null
items: CommentReadDTO[]
constructor(item: CommentItemList) {
this.count = item.count
this.next = item.next
this.prev = item.prev
this.items = item.items.map(_ => new CommentReadDTO(_))
}
}
Loading…
Cancel
Save