diff --git a/backend/app/glossary/controllers.py b/backend/app/glossary/controllers.py index ab8ff3d..d08e3f4 100644 --- a/backend/app/glossary/controllers.py +++ b/backend/app/glossary/controllers.py @@ -38,8 +38,10 @@ def list_glossary_controller(db: Session): return [GlossaryResponse.model_validate(glossary) for glossary in glossaries] -def list_glossary_records_controller(db: Session, glossary_id: int): - records = GlossaryQuery(db).list_glossary_records(glossary_id) +def list_glossary_records_controller( + db: Session, glossary_id: int, page: int, page_records: int +): + records = GlossaryQuery(db).list_glossary_records(glossary_id, page, page_records) return [GlossaryRecordSchema.model_validate(record) for record in records] diff --git a/backend/app/glossary/models.py b/backend/app/glossary/models.py index 9557a57..5268fef 100644 --- a/backend/app/glossary/models.py +++ b/backend/app/glossary/models.py @@ -29,6 +29,10 @@ class Glossary(Base): upload_time: Mapped[datetime] = mapped_column(default=datetime.now) created_by: Mapped[int] = mapped_column(ForeignKey("user.id")) + @property + def records_count(self): + return len(self.records) + records: Mapped[list["GlossaryRecord"]] = relationship( back_populates="glossary", cascade="all, delete-orphan", diff --git a/backend/app/glossary/query.py b/backend/app/glossary/query.py index 9366b9a..1937258 100644 --- a/backend/app/glossary/query.py +++ b/backend/app/glossary/query.py @@ -78,11 +78,13 @@ def get_glossary_records_for_segment( def list_glossary(self) -> list[Glossary]: return self.db.query(Glossary).order_by(Glossary.id).all() - def list_glossary_records(self, glossary_id: int): + def list_glossary_records(self, glossary_id: int, page: int, page_records: int): return ( self.db.query(GlossaryRecord) .filter(GlossaryRecord.glossary_id == glossary_id) # type: ignore .order_by(GlossaryRecord.id) + .offset(page * page_records) + .limit(page_records) .all() ) diff --git a/backend/app/glossary/schema.py b/backend/app/glossary/schema.py index bae4778..f0b4e97 100644 --- a/backend/app/glossary/schema.py +++ b/backend/app/glossary/schema.py @@ -21,6 +21,7 @@ class GlossaryResponse(IdentifiedTimestampedModel): upload_time: datetime.datetime created_by_user: ShortUser name: str + records_count: int model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/routers/glossary.py b/backend/app/routers/glossary.py index 9525409..0ff4b3f 100644 --- a/backend/app/routers/glossary.py +++ b/backend/app/routers/glossary.py @@ -1,10 +1,11 @@ -from typing import Annotated +from typing import Annotated, Final from fastapi import ( APIRouter, BackgroundTasks, Depends, HTTPException, + Query, UploadFile, status, ) @@ -144,12 +145,20 @@ def delete_glossary(glossary_id: int, db: Session = Depends(get_db)): @router.get( "/{glossary_id}/records", - description="Get list glossary record ", + description="Get list glossary record", response_model=list[GlossaryRecordSchema], status_code=status.HTTP_200_OK, ) -def list_records(glossary_id: int, db: Session = Depends(get_db)): - return list_glossary_records_controller(db, glossary_id) +def list_records( + glossary_id: int, + db: Session = Depends(get_db), + page: Annotated[int | None, Query(ge=0)] = None, +): + page_records: Final = 100 + if not page: + page = 0 + + return list_glossary_records_controller(db, glossary_id, page, page_records) @router.post( diff --git a/backend/tests/routers/test_routers_glossary.py b/backend/tests/routers/test_routers_glossary.py index c1fdc9c..59155b6 100644 --- a/backend/tests/routers/test_routers_glossary.py +++ b/backend/tests/routers/test_routers_glossary.py @@ -96,6 +96,40 @@ def test_get_glossary_retrieve(user_logged_client: TestClient, session: Session) assert response_json["id"] == glossary_1.id assert response_json["processing_status"] == glossary_1.processing_status assert response_json["created_by_user"]["id"] == glossary_1.created_by + assert response_json["records_count"] == 0 + + +def test_get_glossary_retrieve_with_records( + user_logged_client: TestClient, session: Session +): + """GET /glossary/{glossary_id}/""" + + glossary_1 = GlossaryQuery(session).create_glossary( + user_id=1, glossary=GlossarySchema(name="Glossary name") + ) + record_scheme = GlossaryRecordCreate( + comment="Comment", + source="Test", + target="Тест", + ) + for _ in range(140): # two pages with 100 records per page + GlossaryQuery(session).create_glossary_record( + user_id=1, + record=record_scheme, + glossary_id=glossary_1.id, + ) + + path = app.url_path_for("retrieve_glossary", **{"glossary_id": glossary_1.id}) + + response = user_logged_client.get(path) + response_json = response.json() + + assert response.status_code == status.HTTP_200_OK + + assert response_json["id"] == glossary_1.id + assert response_json["processing_status"] == glossary_1.processing_status + assert response_json["created_by_user"]["id"] == glossary_1.created_by + assert response_json["records_count"] == 140 def test_update_glossary(user_logged_client: TestClient, session: Session): @@ -143,6 +177,38 @@ def test_list_glossary_records(user_logged_client: TestClient, session: Session) assert resp_rec["glossary_id"] == record.glossary_id +def test_list_glossary_records_paged(user_logged_client: TestClient, session: Session): + """GET /glossary/{glossary_id}/records/""" + + glossary = GlossaryQuery(session).create_glossary( + user_id=1, glossary=GlossarySchema(name="Glossary name") + ) + record_scheme = GlossaryRecordCreate( + comment="Comment", + source="Test", + target="Тест", + ) + + for _ in range(140): # two pages with 100 records per page + record = GlossaryQuery(session).create_glossary_record( + user_id=1, + record=record_scheme, + glossary_id=glossary.id, + ) + + path = app.url_path_for("list_records", **{"glossary_id": glossary.id}) + + response = user_logged_client.get(path, params={"page": 1}) + resp_records = response.json() + + assert resp_records[0]["created_by_user"]["id"] == record.created_by + assert resp_records[0]["id"] == 101 # 100th record + assert resp_records[0]["comment"] == record.comment + assert resp_records[0]["source"] == record.source + assert resp_records[0]["target"] == record.target + assert resp_records[0]["glossary_id"] == record.glossary_id + + def test_update_glossary_record(user_logged_client: TestClient, session: Session): """PUT /glossary/records/{record_id}/""" expected_user_id = 1 diff --git a/frontend/mocks/glossaryMocks.ts b/frontend/mocks/glossaryMocks.ts index 7b07dae..1e10f83 100644 --- a/frontend/mocks/glossaryMocks.ts +++ b/frontend/mocks/glossaryMocks.ts @@ -1,4 +1,7 @@ import {http, HttpResponse} from 'msw' +import {faker} from '@faker-js/faker' +import {fakerRU} from '@faker-js/faker' + import { createGlossaryRecord, deleteGlossary, @@ -16,38 +19,31 @@ import {GlossaryRecordUpdate} from '../src/client/schemas/GlossaryRecordUpdate' import {GlossaryResponse} from '../src/client/schemas/GlossaryResponse' import {GlossarySchema} from '../src/client/schemas/GlossarySchema' +const glossarySegments: GlossaryRecordSchema[] = new Array(125) + .fill(null) + .map((_, idx) => { + return { + id: idx + 1, + glossary_id: 51, + created_at: faker.date.recent().toISOString().split('.')[0], + updated_at: faker.date.recent().toISOString().split('.')[0], + source: faker.commerce.productName(), + target: fakerRU.commerce.productName(), + comment: fakerRU.commerce.productDescription(), + created_by_user: defaultUser, + } + }) + const glossaries: GlossaryResponse[] = [ { id: 51, name: 'Some glossary', - created_at: '2024-12-03T12:31:22', - updated_at: '2024-12-03T16:32:22', + created_at: faker.date.recent().toISOString().split('.')[0], + updated_at: faker.date.recent().toISOString().split('.')[0], processing_status: 'done', - upload_time: '2024-12-03T12:32:05', - created_by_user: defaultUser, - }, -] - -const glossarySegments: GlossaryRecordSchema[] = [ - { - id: 1, - glossary_id: 51, - created_at: '2024-12-03T12:31:22', - updated_at: '2024-12-03T12:31:22', - source: 'Some source', - target: 'Some target', - comment: 'This is a comment', - created_by_user: defaultUser, - }, - { - id: 2, - glossary_id: 51, - created_at: '2024-12-03T12:31:22', - updated_at: '2024-12-03T12:31:22', - source: 'Another source', - target: 'Another target', - comment: 'Some comment from another segment', + upload_time: faker.date.recent().toISOString().split('.')[0], created_by_user: defaultUser, + records_count: glossarySegments.length, }, ] @@ -94,8 +90,16 @@ export const glossaryMocks = [ } } ), - http.get<{id: string}>('http://localhost:8000/glossary/:id/records', () => - HttpResponse.json>(glossarySegments) + http.get<{id: string}>( + 'http://localhost:8000/glossary/:id/records', + ({request}) => { + const searchParams = new URL(request.url).searchParams + const page = Number(searchParams.get('page') ?? '0') + + return HttpResponse.json>( + glossarySegments.slice(page * 100, page * 100 + 100) + ) + } ), http.post<{id: string}, GlossaryRecordCreate>( 'http://localhost:8000/glossary/:id/records', diff --git a/frontend/package.json b/frontend/package.json index bf68362..a33f101 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ }, "homepage": "https://github.com/ArchiDevil/hat#readme", "devDependencies": { + "@faker-js/faker": "^10.0.0", "@vitejs/plugin-vue": "^6.0.1", "@vue/test-utils": "^2.4.6", "autoprefixer": "^10.4.21", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 9238de7..6daf2d5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: specifier: ^4.5.1 version: 4.5.1(vue@3.5.21(typescript@5.3.3)) devDependencies: + '@faker-js/faker': + specifier: ^10.0.0 + version: 10.0.0 '@vitejs/plugin-vue': specifier: ^6.0.1 version: 6.0.1(vite@7.1.6(jiti@1.21.7))(vue@3.5.21(typescript@5.3.3)) @@ -499,6 +502,10 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@faker-js/faker@10.0.0': + resolution: {integrity: sha512-UollFEUkVXutsaP+Vndjxar40Gs5JL2HeLcl8xO1QAjJgOdhc3OmBFWyEylS+RddWaaBiAzH+5/17PLQJwDiLw==} + engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2898,6 +2905,8 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@faker-js/faker@10.0.0': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': diff --git a/frontend/src/client/schemas/GlossaryResponse.ts b/frontend/src/client/schemas/GlossaryResponse.ts index 1b5e6f8..901c23a 100644 --- a/frontend/src/client/schemas/GlossaryResponse.ts +++ b/frontend/src/client/schemas/GlossaryResponse.ts @@ -10,4 +10,5 @@ export interface GlossaryResponse { upload_time: string created_by_user: ShortUser name: string + records_count: number } diff --git a/frontend/src/client/services/GlossaryService.ts b/frontend/src/client/services/GlossaryService.ts index f015ae4..76360f9 100644 --- a/frontend/src/client/services/GlossaryService.ts +++ b/frontend/src/client/services/GlossaryService.ts @@ -26,8 +26,8 @@ export const updateGlossary = async (glossary_id: number, content: GlossarySchem export const deleteGlossary = async (glossary_id: number): Promise => { return await api.delete(`/glossary/${glossary_id}`) } -export const listRecords = async (glossary_id: number): Promise => { - return await api.get(`/glossary/${glossary_id}/records`) +export const listRecords = async (glossary_id: number, page?: number | null): Promise => { + return await api.get(`/glossary/${glossary_id}/records`, {query: {page}}) } export const createGlossaryRecord = async (glossary_id: number, content: GlossaryRecordCreate): Promise => { return await api.post(`/glossary/${glossary_id}/records`, content) diff --git a/frontend/src/stores/current_glossary.ts b/frontend/src/stores/current_glossary.ts index a77b69e..7a72822 100644 --- a/frontend/src/stores/current_glossary.ts +++ b/frontend/src/stores/current_glossary.ts @@ -13,7 +13,11 @@ export const useCurrentGlossaryStore = defineStore('current_glossary', { actions: { async loadGlossary(glossaryId: number) { this.glossary = await retrieveGlossary(glossaryId) - this.records = await listRecords(glossaryId) + this.records = await listRecords(glossaryId, 0) + }, + async loadRecords(page: number | undefined) { + if (!this.glossary) throw new Error('No glossary loaded') + this.records = await listRecords(this.glossary?.id, page ?? 0) }, }, }) diff --git a/frontend/src/views/GlossaryView.vue b/frontend/src/views/GlossaryView.vue index 7e4789d..648891f 100644 --- a/frontend/src/views/GlossaryView.vue +++ b/frontend/src/views/GlossaryView.vue @@ -1,8 +1,9 @@