From 9be2ae42bf0e285b67f43b7db2f3ba8eb4699d58 Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Tue, 9 Dec 2025 01:20:53 +0300 Subject: [PATCH 1/5] Add endpoint to search in linked glossaries --- backend/app/glossary/query.py | 6 +- backend/app/routers/document.py | 28 +++- .../tests/routers/test_routes_documents.py | 122 ++++++++++++++++++ backend/worker.py | 2 +- 4 files changed, 150 insertions(+), 8 deletions(-) diff --git a/backend/app/glossary/query.py b/backend/app/glossary/query.py index 72f87eb..fc28435 100644 --- a/backend/app/glossary/query.py +++ b/backend/app/glossary/query.py @@ -52,10 +52,10 @@ def get_glossary_record_by_id(self, record_id: int) -> GlossaryRecord: return record raise NotFoundGlossaryRecordExc() - def get_glossary_records_for_segment( - self, segment: str, glossary_ids: list[int] + def get_glossary_records_for_phrase( + self, phrase: str, glossary_ids: list[int] ) -> list[GlossaryRecord]: - words = postprocess_stemmed_segment(stem_sentence(segment)) + words = postprocess_stemmed_segment(stem_sentence(phrase)) or_clauses = [GlossaryRecord.source.ilike(f"%{word}%") for word in words] records = self.db.execute( select(GlossaryRecord).where( diff --git a/backend/app/routers/document.py b/backend/app/routers/document.py index 7745e98..b639ee0 100644 --- a/backend/app/routers/document.py +++ b/backend/app/routers/document.py @@ -126,9 +126,9 @@ def get_doc_records( approved=record.approved, repetitions_count=repetitions_count, has_comments=has_comments, - translation_src=record.target_source.value - if record.target_source - else None, + translation_src=( + record.target_source.value if record.target_source else None + ), ) for record, repetitions_count, has_comments in records ] @@ -140,6 +140,26 @@ def get_doc_records( ) +@router.get("/{doc_id}/glossary_search") +def doc_glossary_search( + doc_id: int, + db: Annotated[Session, Depends(get_db)], + query: Annotated[str, Query()], +) -> list[GlossaryRecordSchema]: + doc = get_doc_by_id(db, doc_id) + glossary_ids = [gl.id for gl in doc.glossaries] + return ( + [ + GlossaryRecordSchema.model_validate(record) + for record in GlossaryQuery(db).get_glossary_records_for_phrase( + query, glossary_ids + ) + ] + if glossary_ids + else [] + ) + + @router.get("/records/{record_id}/comments") def get_comments( record_id: int, @@ -200,7 +220,7 @@ def get_record_glossary_records( return ( [ GlossaryRecordSchema.model_validate(record) - for record in GlossaryQuery(db).get_glossary_records_for_segment( + for record in GlossaryQuery(db).get_glossary_records_for_phrase( original_segment.source, glossary_ids ) ] diff --git a/backend/tests/routers/test_routes_documents.py b/backend/tests/routers/test_routes_documents.py index 2c51ed9..e93112b 100644 --- a/backend/tests/routers/test_routes_documents.py +++ b/backend/tests/routers/test_routes_documents.py @@ -934,3 +934,125 @@ def test_get_doc_records_with_repetitions( assert record_counts["Hello World"] == 3 assert record_counts["Goodbye"] == 1 assert record_counts["Test"] == 1 + + +def test_doc_glossary_search_with_matching_records( + user_logged_client: TestClient, session: Session +): + with session as s: + records = [ + DocumentRecord(source="Regional Effects", target=""), + DocumentRecord(source="User Interface", target=""), + ] + s.add( + Document( + name="test_doc.txt", + type=DocumentType.txt, + records=records, + processing_status="pending", + created_by=1, + ) + ) + s.commit() + + gq = GlossaryQuery(session) + g = gq.create_glossary(1, GlossarySchema(name="test"), ProcessingStatuses.DONE) + gq.create_glossary_record( + 1, + GlossaryRecordCreate( + comment=None, source="Regional Effects", target="Региональные эффекты" + ), + g.id, + ) + gq.create_glossary_record( + 1, + GlossaryRecordCreate( + comment=None, source="User Interface", target="Пользовательский интерфейс" + ), + g.id, + ) + + dq = GenericDocsQuery(session) + dq.set_document_glossaries(dq.get_document(1), [g]) + + response = user_logged_client.get( + "/document/1/glossary_search?query=Regional Effects" + ) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["source"] == "Regional Effects" + assert response_json[0]["target"] == "Региональные эффекты" + assert response_json[0]["glossary_id"] == 1 + assert response_json[0]["comment"] is None + assert response_json[0]["created_by_user"]["id"] == 1 + + +def test_doc_glossary_search_returns_empty_when_no_glossaries( + user_logged_client: TestClient, session: Session +): + with session as s: + records = [ + DocumentRecord(source="Regional Effects", 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/glossary_search?query=Regional Effects" + ) + assert response.status_code == 200 + assert response.json() == [] + + +def test_doc_glossary_search_returns_empty_when_no_matches( + user_logged_client: TestClient, session: Session +): + with session as s: + records = [ + DocumentRecord(source="Regional Effects", target=""), + ] + s.add( + Document( + name="test_doc.txt", + type=DocumentType.txt, + records=records, + processing_status="pending", + created_by=1, + ) + ) + s.commit() + + gq = GlossaryQuery(session) + g = gq.create_glossary(1, GlossarySchema(name="test"), ProcessingStatuses.DONE) + gq.create_glossary_record( + 1, + GlossaryRecordCreate( + comment=None, source="Regional Effects", target="Региональные эффекты" + ), + g.id, + ) + + dq = GenericDocsQuery(session) + dq.set_document_glossaries(dq.get_document(1), [g]) + + response = user_logged_client.get( + "/document/1/glossary_search?query=Nonexistent Term" + ) + assert response.status_code == 200 + assert response.json() == [] + + +def test_doc_glossary_search_returns_404_for_nonexistent_document( + user_logged_client: TestClient, +): + response = user_logged_client.get("/document/99/glossary_search?query=test") + assert response.status_code == 404 diff --git a/backend/worker.py b/backend/worker.py index a47a2df..dd0f9ef 100644 --- a/backend/worker.py +++ b/backend/worker.py @@ -262,7 +262,7 @@ def translate_segments( ] data_to_translate: list[LineWithGlossaries] = [] for segment in segments_to_translate: - glossary_records = GlossaryQuery(session).get_glossary_records_for_segment( + glossary_records = GlossaryQuery(session).get_glossary_records_for_phrase( segment, glossary_ids ) data_to_translate.append( From 748c940a1f6618f8a15816dc18fa428f12a61c8d Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Tue, 9 Dec 2025 01:20:59 +0300 Subject: [PATCH 2/5] Generate client --- frontend/src/client/services/DocumentService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/client/services/DocumentService.ts b/frontend/src/client/services/DocumentService.ts index 5e77025..ac4c6a2 100644 --- a/frontend/src/client/services/DocumentService.ts +++ b/frontend/src/client/services/DocumentService.ts @@ -7,10 +7,10 @@ import {Document} from '../schemas/Document' import {Body_create_doc_document__post} from '../schemas/Body_create_doc_document__post' import {StatusMessage} from '../schemas/StatusMessage' import {DocumentRecordListResponse} from '../schemas/DocumentRecordListResponse' +import {GlossaryRecordSchema} from '../schemas/GlossaryRecordSchema' import {CommentResponse} from '../schemas/CommentResponse' import {CommentCreate} from '../schemas/CommentCreate' import {MemorySubstitution} from '../schemas/MemorySubstitution' -import {GlossaryRecordSchema} from '../schemas/GlossaryRecordSchema' import {DocumentRecordUpdateResponse} from '../schemas/DocumentRecordUpdateResponse' import {DocumentRecordUpdate} from '../schemas/DocumentRecordUpdate' import {DocTranslationMemory} from '../schemas/DocTranslationMemory' @@ -38,6 +38,9 @@ export const deleteDoc = async (doc_id: number): Promise => { export const getDocRecords = async (doc_id: number, page?: number | null, source?: string | null, target?: string | null): Promise => { return await api.get(`/document/${doc_id}/records`, {query: {page, source, target}}) } +export const docGlossarySearch = async (doc_id: number, query: string): Promise => { + return await api.get(`/document/${doc_id}/glossary_search`, {query: {query}}) +} export const getComments = async (record_id: number): Promise => { return await api.get(`/document/records/${record_id}/comments`) } From 3643f50d7c91c45774aeea4d415dfcc59745ca7b Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Thu, 11 Dec 2025 00:59:42 +0300 Subject: [PATCH 3/5] Search for terms while adding a glossary record --- .../src/components/glossary/AddTermDialog.vue | 55 +++++++++++++++++-- .../components/glossary/EditTermDialog.vue | 7 ++- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/glossary/AddTermDialog.vue b/frontend/src/components/glossary/AddTermDialog.vue index 3684211..76cb729 100644 --- a/frontend/src/components/glossary/AddTermDialog.vue +++ b/frontend/src/components/glossary/AddTermDialog.vue @@ -1,7 +1,13 @@ diff --git a/frontend/src/components/glossary/EditTermDialog.vue b/frontend/src/components/glossary/EditTermDialog.vue index a8a1024..2ac106c 100644 --- a/frontend/src/components/glossary/EditTermDialog.vue +++ b/frontend/src/components/glossary/EditTermDialog.vue @@ -55,8 +55,8 @@ const deleteRecord = async () => {
@@ -72,6 +72,7 @@ const deleteRecord = async () => { class="flex-auto" autocomplete="off" placeholder="Input source term" + disabled />
@@ -101,7 +102,7 @@ const deleteRecord = async () => { v-model="comment" class="flex-auto" autocomplete="off" - placeholder="(Optional) Comment for a term" + placeholder="(Optional) Input comment" />
From 2a74221cdb71677b0240372739003e656b9c31e6 Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Thu, 11 Dec 2025 22:07:13 +0300 Subject: [PATCH 4/5] Clear data when term is added --- frontend/src/components/glossary/AddTermDialog.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/glossary/AddTermDialog.vue b/frontend/src/components/glossary/AddTermDialog.vue index 76cb729..17695fb 100644 --- a/frontend/src/components/glossary/AddTermDialog.vue +++ b/frontend/src/components/glossary/AddTermDialog.vue @@ -32,6 +32,14 @@ watch(source, (newVal) => { const target = ref('') const comment = ref('') +const clearData = () => { + model.value = false + source.value = '' + debouncedSearch.value = '' + target.value = '' + comment.value = '' +} + const submit = async () => { await createGlossaryRecord(glossaryId, { source: source.value, @@ -39,7 +47,7 @@ const submit = async () => { comment: comment.value, }) window.umami.track('glossary-add') - model.value = false + clearData() emit('close') } @@ -58,6 +66,7 @@ const {data: foundTerms} = useQuery({ modal header="Add Term" :style="{width: '40rem'}" + @hide="clearData" >
From 50bc8242ae4fcfa77e49ee8c44a3dc11c543d9ce Mon Sep 17 00:00:00 2001 From: Denis Bezykornov Date: Thu, 11 Dec 2025 22:07:55 +0300 Subject: [PATCH 5/5] Add modal to add terms from doc view --- frontend/mocks/documentMocks.ts | 21 +- frontend/mocks/glossaryMocks.ts | 2 +- .../src/components/document/AddTermModal.vue | 205 ++++++++++++++++++ .../{FilterPanel.vue => ToolsPanel.vue} | 14 +- frontend/src/views/DocView.vue | 14 +- 5 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/document/AddTermModal.vue rename frontend/src/components/document/{FilterPanel.vue => ToolsPanel.vue} (86%) diff --git a/frontend/mocks/documentMocks.ts b/frontend/mocks/documentMocks.ts index c549200..b0ba6b5 100644 --- a/frontend/mocks/documentMocks.ts +++ b/frontend/mocks/documentMocks.ts @@ -1,12 +1,14 @@ import {http, HttpResponse} from 'msw' import {faker, fakerRU} from '@faker-js/faker' +import {glossaries} from './glossaryMocks' import {AwaitedReturnType} from './utils' import { getComments, getDoc, getDocRecords, getDocs, + getGlossaries, getRecordGlossaryRecords, getRecordSubstitutions, updateDocRecord, @@ -209,7 +211,7 @@ const segments: DocumentRecord[] = [ has_comments: true, translation_src: 'mt', }, - { + { id: 10003, approved: true, source: 'Adventure Hooks', @@ -263,6 +265,22 @@ export const documentMocks = [ } } ), + http.get<{id: string}>( + 'http://localhost:8000/document/:id/glossaries', + ({params}) => { + const doc = docs.find((doc) => doc.id === Number(params.id)) + if (doc !== undefined) { + return HttpResponse.json>([ + { + document_id: doc.id, + glossary: glossaries[0], + }, + ]) + } else { + return new HttpResponse(null, {status: 404}) + } + } + ), http.put<{id: string}, DocumentRecordUpdate>( 'http://localhost:8000/document/record/:id', async ({params, request}) => { @@ -345,7 +363,6 @@ export const documentMocks = [ } } ), - http.get<{segmentId: string}>( 'http://localhost:8000/document/records/:segmentId/comments', ({params}) => { diff --git a/frontend/mocks/glossaryMocks.ts b/frontend/mocks/glossaryMocks.ts index 9e311ae..a4ca700 100644 --- a/frontend/mocks/glossaryMocks.ts +++ b/frontend/mocks/glossaryMocks.ts @@ -34,7 +34,7 @@ const glossarySegments: GlossaryRecordSchema[] = new Array(125) } }) -const glossaries: GlossaryResponse[] = [ +export const glossaries: GlossaryResponse[] = [ { id: 51, name: 'Some glossary', diff --git a/frontend/src/components/document/AddTermModal.vue b/frontend/src/components/document/AddTermModal.vue new file mode 100644 index 0000000..651dd2d --- /dev/null +++ b/frontend/src/components/document/AddTermModal.vue @@ -0,0 +1,205 @@ + + + diff --git a/frontend/src/components/document/FilterPanel.vue b/frontend/src/components/document/ToolsPanel.vue similarity index 86% rename from frontend/src/components/document/FilterPanel.vue rename to frontend/src/components/document/ToolsPanel.vue index 5d889af..9c40dad 100644 --- a/frontend/src/components/document/FilterPanel.vue +++ b/frontend/src/components/document/ToolsPanel.vue @@ -1,13 +1,13 @@ diff --git a/frontend/src/views/DocView.vue b/frontend/src/views/DocView.vue index c0b2a74..f5960d8 100644 --- a/frontend/src/views/DocView.vue +++ b/frontend/src/views/DocView.vue @@ -12,9 +12,11 @@ import SubstitutionsList from '../components/document/SubstitutionsList.vue' import ProcessingErrorMessage from '../components/document/ProcessingErrorMessage.vue' import RoutingLink from '../components/RoutingLink.vue' import DocumentSkeleton from '../components/document/DocumentSkeleton.vue' -import FilterPanel from '../components/document/FilterPanel.vue' +import ToolsPanel from '../components/document/ToolsPanel.vue' import TmSearchModal from '../components/TmSearchModal.vue' import RecordCommentModal from '../components/document/RecordCommentModal.vue' +import AddTermModal from '../components/document/AddTermModal.vue' + import { getDoc, getDocRecords, @@ -200,6 +202,8 @@ const onAddComment = (recordId: number) => { commentsRecordId.value = recordId showCommentsModal.value = true } + +const showAddTermModal = ref(false) -
{ :record-id="commentsRecordId ?? -1" @add-comment="refetchRecords" /> + +