From 8376bcf2b80c3a5d87b0d30984cea10ecc8ea650 Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Sat, 18 Oct 2025 00:24:19 +0300 Subject: [PATCH 1/4] Add search for segment repetitions (GLM) --- backend/app/documents/query.py | 26 +++++++++++--- backend/app/documents/schema.py | 1 + backend/app/routers/document.py | 3 +- .../tests/routers/test_routes_documents.py | 36 +++++++++++++++++++ 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/backend/app/documents/query.py b/backend/app/documents/query.py index 2cbaa2a..ed8b52a 100644 --- a/backend/app/documents/query.py +++ b/backend/app/documents/query.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Iterable -from sqlalchemy import case, func, select +from sqlalchemy import case, func, select, Row from sqlalchemy.orm import Session from app.base.exceptions import BaseQueryException @@ -91,14 +91,32 @@ def get_document_records_count(self, doc: Document) -> tuple[int, int]: def get_document_records_paged( self, doc: Document, page: int, page_records=100 - ) -> Iterable[DocumentRecord]: + ) -> Iterable[Row[tuple[DocumentRecord, int]]]: + # Subquery to count repetitions for each source text within the document + repetitions_subquery = ( + select( + DocumentRecord.source, + func.count(DocumentRecord.id).label('repetitions_count') + ) + .filter(DocumentRecord.document_id == doc.id) + .group_by(DocumentRecord.source) + .subquery() + ) + return self.__db.execute( - select(DocumentRecord) + select( + DocumentRecord, + func.coalesce(repetitions_subquery.c.repetitions_count, 0).label('repetitions_count') + ) .filter(DocumentRecord.document_id == doc.id) + .outerjoin( + repetitions_subquery, + DocumentRecord.source == repetitions_subquery.c.source + ) .order_by(DocumentRecord.id) .offset(page_records * page) .limit(page_records) - ).scalars() + ).all() def get_record(self, record_id: int) -> DocumentRecord | None: return self.__db.execute( diff --git a/backend/app/documents/schema.py b/backend/app/documents/schema.py index 502cef2..f4eb5ea 100644 --- a/backend/app/documents/schema.py +++ b/backend/app/documents/schema.py @@ -24,6 +24,7 @@ class DocumentRecord(Identified): source: str target: str approved: bool + repetitions_count: int class DocumentRecordUpdate(BaseModel): diff --git a/backend/app/routers/document.py b/backend/app/routers/document.py index b423a36..66b05a9 100644 --- a/backend/app/routers/document.py +++ b/backend/app/routers/document.py @@ -96,8 +96,9 @@ def get_doc_records( source=record.source, target=record.target, approved=record.approved, + repetitions_count=repetitions_count, ) - for record in records + for record, repetitions_count in records ] diff --git a/backend/tests/routers/test_routes_documents.py b/backend/tests/routers/test_routes_documents.py index 8476cfb..3e423c9 100644 --- a/backend/tests/routers/test_routes_documents.py +++ b/backend/tests/routers/test_routes_documents.py @@ -915,3 +915,39 @@ def test_setting_glossaries_returns_404_for_non_existing_glossaries( json={"glossaries": [{"id": 99}]}, ) assert response.status_code == 404 + + +def test_get_doc_records_with_repetitions(user_logged_client: TestClient, session: Session): + """Test that document records endpoint returns repetition counts""" + with session as s: + records = [ + DocumentRecord(source="Hello World", target="Привет Мир"), + DocumentRecord(source="Goodbye", target="Пока"), + DocumentRecord(source="Hello World", target="Здравствуйте Мир"), + DocumentRecord(source="Test", target="Тест"), + DocumentRecord(source="Hello World", target="Хелло Ворлд"), + ] + s.add( + Document( + name="test_doc.txt", + type=DocumentType.txt, + records=records, + processing_status="pending", + created_by=1, + ) + ) + s.commit() + + response = user_logged_client.get("/document/1/records") + assert response.status_code == 200 + response_json = response.json() + + # Should return all 5 records + assert len(response_json) == 5 + + # Check that repetition counts are correct + # "Hello World" appears 3 times, others appear once + record_counts = {record["source"]: record["repetitions_count"] for record in response_json} + assert record_counts["Hello World"] == 3 + assert record_counts["Goodbye"] == 1 + assert record_counts["Test"] == 1 From aa770605cb1dc29876be559a25f70361f31191af Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Sun, 19 Oct 2025 19:36:37 +0300 Subject: [PATCH 2/4] Generate new client --- frontend/src/client/schemas/DocumentRecord.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/client/schemas/DocumentRecord.ts b/frontend/src/client/schemas/DocumentRecord.ts index 02d98ea..db1df65 100644 --- a/frontend/src/client/schemas/DocumentRecord.ts +++ b/frontend/src/client/schemas/DocumentRecord.ts @@ -5,4 +5,5 @@ export interface DocumentRecord { source: string target: string approved: boolean + repetitions_count: number } From 051edfbf1c2f4b603ca5026c70d5d81b02317b0e Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Sun, 19 Oct 2025 19:27:53 +0300 Subject: [PATCH 3/4] Add mark for segments if they are repeated (GLM) --- frontend/mocks/documentMocks.ts | 74 ++++++++++++++++---- frontend/src/components/DocSegment.test.ts | 78 ++++++++++++++++++++++ frontend/src/components/DocSegment.vue | 8 ++- frontend/src/stores/current_document.ts | 1 + frontend/src/views/DocView.vue | 1 + 5 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/DocSegment.test.ts diff --git a/frontend/mocks/documentMocks.ts b/frontend/mocks/documentMocks.ts index f180f0b..8251a74 100644 --- a/frontend/mocks/documentMocks.ts +++ b/frontend/mocks/documentMocks.ts @@ -1,19 +1,69 @@ import {http, HttpResponse} from 'msw' import {AwaitedReturnType} from './utils' -import {getDocs} from '../src/client/services/DocumentService' +import { + getDoc, + getDocRecords, + getDocs, +} from '../src/client/services/DocumentService' +import {DocumentStatus} from '../src/client/schemas/DocumentStatus' + +const docs = [ + { + id: 1, + created_by: 12, + records_count: 3, + approved_records_count: 1, + name: 'Some document', + status: 'done' as DocumentStatus, + type: 'XLIFF', + }, +] export const documentMocks = [ http.get('http://localhost:8000/document/', () => - HttpResponse.json>([ - { - id: 1, - created_by: 12, - records_count: 450, - approved_records_count: 247, - name: 'Some document', - status: 'done', - type: 'XLIFF', - }, - ]) + HttpResponse.json>(docs) + ), + http.get<{id: string}>('http://localhost:8000/document/:id', ({params}) => { + const doc = docs.find((doc) => doc.id === Number(params.id)) + if (doc !== undefined) { + return HttpResponse.json>(doc) + } else { + new HttpResponse(null, {status: 404}) + } + }), + http.get<{id: string}>( + 'http://localhost:8000/document/:id/records', + ({params}) => { + const doc = docs.find((doc) => doc.id === Number(params.id)) + if (doc !== undefined) { + return HttpResponse.json>([ + { + id: 1, + approved: false, + source: 'Adventure Hooks', + target: 'Зацепки приключения', + repetitions_count: 2, + }, + { + id: 2, + approved: true, + source: 'Adventure Hooks', + target: 'Зацепки приключения', + repetitions_count: 2, + }, + { + id: 3, + approved: false, + source: + 'The moment the Cynidiceans pried the horn from the monolith, their city was doomed.', + target: + 'В тот момент, когда кинидийцы извлекли рог из монолита, их город был обречен.', + repetitions_count: 1, + }, + ]) + } else { + new HttpResponse(null, {status: 404}) + } + } ), ] diff --git a/frontend/src/components/DocSegment.test.ts b/frontend/src/components/DocSegment.test.ts new file mode 100644 index 0000000..e0c688b --- /dev/null +++ b/frontend/src/components/DocSegment.test.ts @@ -0,0 +1,78 @@ +import {mount} from '@vue/test-utils' +import {describe, it, expect} from 'vitest' +import DocSegment from './DocSegment.vue' + +describe('DocSegment', () => { + it('renders segment ID correctly', () => { + const wrapper = mount(DocSegment, { + props: { + id: 123, + source: 'Test source', + target: 'Test target', + editable: true, + }, + }) + + expect(wrapper.text()).toContain('123') + }) + + it('shows repetition dot when repetitionsCount > 1 and editable', () => { + const wrapper = mount(DocSegment, { + props: { + id: 123, + source: 'Test source', + target: 'Test target', + editable: true, + repetitionsCount: 3, + }, + }) + + const dot = wrapper.find('.bg-orange-500') + expect(dot.exists()).toBe(true) + expect(dot.attributes('title')).toBe('Repeated 3 times') + }) + + it('does not show repetition dot when repetitionsCount = 1', () => { + const wrapper = mount(DocSegment, { + props: { + id: 123, + source: 'Test source', + target: 'Test target', + editable: true, + repetitionsCount: 1, + }, + }) + + const dot = wrapper.find('.bg-orange-500') + expect(dot.exists()).toBe(false) + }) + + it('does not show repetition dot when not editable', () => { + const wrapper = mount(DocSegment, { + props: { + id: 123, + source: 'Test source', + target: 'Test target', + editable: false, + repetitionsCount: 3, + }, + }) + + const dot = wrapper.find('.bg-orange-500') + expect(dot.exists()).toBe(false) + }) + + it('does not show repetition dot when repetitionsCount is undefined', () => { + const wrapper = mount(DocSegment, { + props: { + id: 123, + source: 'Test source', + target: 'Test target', + editable: true, + }, + }) + + const dot = wrapper.find('.bg-orange-500') + expect(dot.exists()).toBe(false) + }) +}) diff --git a/frontend/src/components/DocSegment.vue b/frontend/src/components/DocSegment.vue index b8d3b77..dd4115f 100644 --- a/frontend/src/components/DocSegment.vue +++ b/frontend/src/components/DocSegment.vue @@ -13,6 +13,7 @@ const props = defineProps<{ editable?: boolean disabled?: boolean approved?: boolean + repetitionsCount?: number }>() const emit = defineEmits<{ @@ -67,8 +68,13 @@ watchEffect(() => {