Browse Source

Merge pull request #2228 from doccano/enhancement/label-filter

[Enhancement] Support dataset filtering by label name
pull/2233/head
Hiroki Nakayama 2 years ago
committed by GitHub
parent
commit
f7c550913a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 75 additions and 7 deletions
  1. 36
      backend/examples/filters.py
  2. 13
      backend/examples/tests/test_filters.py
  3. 2
      frontend/components/example/AudioList.vue
  4. 2
      frontend/components/example/DocumentList.vue
  5. 2
      frontend/components/example/ImageList.vue
  6. 27
      frontend/repositories/example/apiDocumentRepository.ts

36
backend/examples/filters.py

@ -1,11 +1,12 @@
from django.db.models import Count, Q
from django_filters.rest_framework import BooleanFilter, FilterSet
from django.db.models import Count, Q, QuerySet
from django_filters.rest_framework import BooleanFilter, CharFilter, FilterSet
from .models import Example
class ExampleFilter(FilterSet):
confirmed = BooleanFilter(field_name="states", method="filter_by_state")
label = CharFilter(method="filter_by_label")
def filter_by_state(self, queryset, field_name, is_confirmed: bool):
queryset = queryset.annotate(
@ -21,6 +22,35 @@ class ExampleFilter(FilterSet):
queryset = queryset.filter(num_confirm__lte=0)
return queryset
def filter_by_label(self, queryset: QuerySet, field_name: str, label: str) -> QuerySet:
"""Filter examples by a given label name.
This performs filtering on all of the following labels at once:
- categories
- spans
- relations
- bboxes
- segmentations
Todo: Consider project type to make filtering more efficient.
Args:
queryset (QuerySet): QuerySet to filter.
field_name (str): This equals to `label`.
label (str): The label name to filter.
Returns:
QuerySet: Filtered examples.
"""
queryset = queryset.filter(
Q(categories__label__text=label)
| Q(spans__label__text=label)
| Q(relations__type__text=label)
| Q(bboxes__label__text=label)
| Q(segmentations__label__text=label)
)
return queryset
class Meta:
model = Example
fields = ("project", "text", "created_at", "updated_at")
fields = ("project", "text", "created_at", "updated_at", "label")

13
backend/examples/tests/test_filters.py

@ -1,10 +1,12 @@
from unittest.mock import MagicMock
from django.test import TestCase
from model_mommy import mommy
from .utils import make_doc, make_example_state
from examples.filters import ExampleFilter
from examples.models import Example
from projects.models import ProjectType
from projects.tests.utils import prepare_project
@ -48,6 +50,17 @@ class TestExampleFilter(TestFilterMixin):
self.assert_filter(data={"confirmed": ""}, expected=1)
class TestLabelFilter(TestFilterMixin):
def setUp(self):
self.project = prepare_project(task=ProjectType.DOCUMENT_CLASSIFICATION)
self.prepare(project=self.project)
self.label_type = mommy.make("CategoryType", project=self.project.item, text="positive")
mommy.make("Category", example=self.example, label=self.label_type)
def test_returns_example_with_positive_label(self):
self.assert_filter(data={"label": self.label_type.text}, expected=1)
class TestExampleFilterOnCollaborative(TestFilterMixin):
def setUp(self):
self.project = prepare_project(task="DocumentClassification", collaborative_annotation=True)

2
frontend/components/example/AudioList.vue

@ -23,7 +23,7 @@
<v-text-field
v-model="search"
:prepend-inner-icon="mdiMagnify"
:label="$t('generic.search')"
:label="$t('generic.search') + ' (e.g. label:positive)'"
single-line
hide-details
filled

2
frontend/components/example/DocumentList.vue

@ -23,7 +23,7 @@
<v-text-field
v-model="search"
:prepend-inner-icon="mdiMagnify"
:label="$t('generic.search')"
:label="$t('generic.search') + ' (e.g. label:positive)'"
single-line
hide-details
filled

2
frontend/components/example/ImageList.vue

@ -23,7 +23,7 @@
<v-text-field
v-model="search"
:prepend-inner-icon="mdiMagnify"
:label="$t('generic.search')"
:label="$t('generic.search') + ' (e.g. label:positive)'"
single-line
hide-details
filled

27
frontend/repositories/example/apiDocumentRepository.ts

@ -25,6 +25,29 @@ function toPayload(item: ExampleItem): { [key: string]: any } {
}
}
function buildQueryParams(
limit: any,
offset: string,
q: string,
isChecked: string,
ordering: string
): string {
const params = new URLSearchParams()
params.append('limit', limit)
params.append('offset', offset)
params.append('confirmed', isChecked)
params.append('ordering', ordering)
const pattern = /label:(".+?"|\S+)/
if (pattern.test(q)) {
const label = pattern.exec(q)![1]
params.append('label', label.replace(/"/g, ''))
q = q.replace(pattern, '')
}
params.append('q', q)
return params.toString()
}
export class APIExampleRepository implements ExampleRepository {
constructor(private readonly request = ApiService) {}
@ -32,7 +55,9 @@ export class APIExampleRepository implements ExampleRepository {
projectId: string,
{ limit = '10', offset = '0', q = '', isChecked = '', ordering = '' }: SearchOption
): Promise<ExampleItemList> {
const url = `/projects/${projectId}/examples?limit=${limit}&offset=${offset}&q=${q}&confirmed=${isChecked}&ordering=${ordering}`
// @ts-ignore
const params = buildQueryParams(limit, offset, q, isChecked, ordering)
const url = `/projects/${projectId}/examples?${params}`
const response = await this.request.get(url)
return new ExampleItemList(
response.data.count,

Loading…
Cancel
Save