diff --git a/backend/api/tests/api/test_project.py b/backend/api/tests/api/test_project.py index cf8efc3a..960d68af 100644 --- a/backend/api/tests/api/test_project.py +++ b/backend/api/tests/api/test_project.py @@ -15,13 +15,13 @@ class TestProjectList(CRUDMixin): def test_return_projects_to_member(self): for member in self.project.members: response = self.assert_fetch(member, status.HTTP_200_OK) - project = response.data[0] - self.assertEqual(len(response.data), 1) + project = response.data['results'][0] + self.assertEqual(response.data['count'], 1) self.assertEqual(project['id'], self.project.item.id) def test_does_not_return_project_to_non_member(self): response = self.assert_fetch(self.non_member, status.HTTP_200_OK) - self.assertEqual(len(response.data), 0) + self.assertEqual(response.data['count'], 0) class TestProjectCreate(CRUDMixin): diff --git a/backend/api/views/project.py b/backend/api/views/project.py index 57ac61c0..6f06bca9 100644 --- a/backend/api/views/project.py +++ b/backend/api/views/project.py @@ -1,5 +1,6 @@ from django.conf import settings -from rest_framework import generics, status +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, generics, status from rest_framework.permissions import IsAdminUser, IsAuthenticated from rest_framework.response import Response @@ -11,7 +12,8 @@ from ..serializers import ProjectPolymorphicSerializer class ProjectList(generics.ListCreateAPIView): serializer_class = ProjectPolymorphicSerializer - pagination_class = None + filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) + search_fields = ('name', 'description') def get_permissions(self): if self.request.method == 'GET': diff --git a/frontend/components/project/ProjectList.vue b/frontend/components/project/ProjectList.vue index 0cc4e9a2..7e129cbd 100644 --- a/frontend/components/project/ProjectList.vue +++ b/frontend/components/project/ProjectList.vue @@ -3,12 +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': [10, 50, 100], 'items-per-page-text': $t('vuetify.itemsPerPageText'), 'page-text': $t('dataset.pageText') }" @@ -36,19 +39,21 @@ diff --git a/frontend/domain/models/project/project.ts b/frontend/domain/models/project/project.ts index 36ccef5e..dc0eedcc 100644 --- a/frontend/domain/models/project/project.ts +++ b/frontend/domain/models/project/project.ts @@ -1,4 +1,5 @@ -import { Expose } from 'class-transformer' +import "reflect-metadata" +import { Expose, Type } from 'class-transformer' export type ProjectType = 'DocumentClassification' | 'SequenceLabeling' | 'Seq2seq' | 'IntentDetectionAndSlotFilling' | 'ImageClassification' | 'Speech2text' @@ -74,6 +75,16 @@ export class ProjectReadItem { } } +export class ProjectItemList { + count: number; + next: string | null; + prev: string | null; + + @Type(() => ProjectReadItem) + @Expose({ name: 'results' }) + items: ProjectReadItem[]; +} + export class ProjectWriteItem { constructor( public id: number, diff --git a/frontend/domain/models/project/projectRepository.ts b/frontend/domain/models/project/projectRepository.ts index ebba0e17..223ea017 100644 --- a/frontend/domain/models/project/projectRepository.ts +++ b/frontend/domain/models/project/projectRepository.ts @@ -1,8 +1,9 @@ -import { ProjectReadItem, ProjectWriteItem } from '~/domain/models/project/project' +import { ProjectReadItem, ProjectWriteItem, ProjectItemList } from '~/domain/models/project/project' +export type SearchOption = {[key: string]: string | (string | null)[]} export interface ProjectRepository { - list(): Promise + list({ limit, offset, q }: SearchOption): Promise findById(id: string): Promise diff --git a/frontend/pages/projects/index.vue b/frontend/pages/projects/index.vue index 24e0fe83..4b7c99ce 100644 --- a/frontend/pages/projects/index.vue +++ b/frontend/pages/projects/index.vue @@ -33,17 +33,20 @@ + :total="projects.count" + @update:query="updateQuery" + /> diff --git a/frontend/repositories/project/apiProjectRepository.ts b/frontend/repositories/project/apiProjectRepository.ts index b9ad2e24..6da0810f 100644 --- a/frontend/repositories/project/apiProjectRepository.ts +++ b/frontend/repositories/project/apiProjectRepository.ts @@ -1,7 +1,7 @@ import { plainToInstance } from 'class-transformer' import ApiService from '@/services/api.service' -import { ProjectRepository } from '@/domain/models/project/projectRepository' -import { ProjectReadItem, ProjectWriteItem } from '~/domain/models/project/project' +import { ProjectRepository, SearchOption } from '@/domain/models/project/projectRepository' +import { ProjectReadItem, ProjectWriteItem, ProjectItemList } from '~/domain/models/project/project' export class APIProjectRepository implements ProjectRepository { @@ -9,10 +9,10 @@ export class APIProjectRepository implements ProjectRepository { private readonly request = ApiService ) {} - async list(): Promise { - const url = `/projects` + async list({ limit = '10', offset = '0', q = '' }: SearchOption): Promise { + const url = `/projects?limit=${limit}&offset=${offset}&q=${q}` const response = await this.request.get(url) - return response.data.map((item: any) => plainToInstance(ProjectReadItem, item)) + return plainToInstance(ProjectItemList, response.data) } async findById(id: string): Promise { diff --git a/frontend/services/application/project/projectApplicationService.ts b/frontend/services/application/project/projectApplicationService.ts index 74adebad..0f490995 100644 --- a/frontend/services/application/project/projectApplicationService.ts +++ b/frontend/services/application/project/projectApplicationService.ts @@ -1,5 +1,5 @@ -import { ProjectDTO, ProjectWriteDTO } from './projectData' -import { ProjectRepository } from '~/domain/models/project/projectRepository' +import { ProjectDTO, ProjectWriteDTO, ProjectListDTO } from './projectData' +import { ProjectRepository, SearchOption } from '~/domain/models/project/projectRepository' import { ProjectWriteItem } from '~/domain/models/project/project' @@ -8,10 +8,10 @@ export class ProjectApplicationService { private readonly repository: ProjectRepository ) {} - public async list(): Promise { + public async list(options: SearchOption): Promise { try { - const items = await this.repository.list() - return items.map(item => new ProjectDTO(item)) + const items = await this.repository.list(options) + return new ProjectListDTO(items) } catch(e: any) { throw new Error(e.response.data.detail) } diff --git a/frontend/services/application/project/projectData.ts b/frontend/services/application/project/projectData.ts index dd6ba5d8..e1800cea 100644 --- a/frontend/services/application/project/projectData.ts +++ b/frontend/services/application/project/projectData.ts @@ -1,4 +1,4 @@ -import { ProjectReadItem, ProjectType } from '~/domain/models/project/project' +import { ProjectReadItem, ProjectType, ProjectItemList } from '~/domain/models/project/project' export class ProjectDTO { id: number @@ -45,3 +45,17 @@ export class ProjectDTO { } export type ProjectWriteDTO = Pick + +export class ProjectListDTO { + count: number + next : string | null + prev : string | null + items: ProjectDTO[] + + constructor(item: ProjectItemList) { + this.count = item.count + this.next = item.next + this.prev = item.prev + this.items = item.items.map(_ => new ProjectDTO(_)) + } +}