Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions backend/app/documents/query.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 7 additions & 0 deletions backend/app/documents/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class DocumentRecord(Identified):
source: str
target: str
approved: bool
repetitions_count: int


class DocumentRecordUpdateResponse(Identified):
source: str
target: str
approved: bool


class DocumentRecordUpdate(BaseModel):
Expand Down
5 changes: 3 additions & 2 deletions backend/app/routers/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]


Expand Down Expand Up @@ -149,7 +150,7 @@ def update_doc_record(
record_id: int,
record: doc_schema.DocumentRecordUpdate,
db: Annotated[Session, Depends(get_db)],
) -> doc_schema.DocumentRecord:
) -> doc_schema.DocumentRecordUpdateResponse:
try:
record = GenericDocsQuery(db).update_record(record_id, record)
return record
Expand Down
3 changes: 3 additions & 0 deletions backend/tests/routers/test_routes_doc_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ def test_can_get_doc_records(user_logged_client: TestClient, session: Session):
"source": "Regional Effects",
"target": "Translation",
"approved": False,
"repetitions_count": 1,
},
{
"id": 2,
"source": "User Interface",
"target": "UI",
"approved": True,
"repetitions_count": 1,
},
]

Expand Down Expand Up @@ -81,6 +83,7 @@ def test_doc_records_returns_second_page(
"source": "line100",
"target": "line100",
"approved": False,
"repetitions_count": 1,
}


Expand Down
36 changes: 36 additions & 0 deletions backend/tests/routers/test_routes_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
74 changes: 62 additions & 12 deletions frontend/mocks/documentMocks.ts
Original file line number Diff line number Diff line change
@@ -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<AwaitedReturnType<typeof getDocs>>([
{
id: 1,
created_by: 12,
records_count: 450,
approved_records_count: 247,
name: 'Some document',
status: 'done',
type: 'XLIFF',
},
])
HttpResponse.json<AwaitedReturnType<typeof getDocs>>(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<AwaitedReturnType<typeof getDoc>>(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<AwaitedReturnType<typeof getDocRecords>>([
{
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})
}
}
),
]
1 change: 1 addition & 0 deletions frontend/src/client/schemas/DocumentRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface DocumentRecord {
source: string
target: string
approved: boolean
repetitions_count: number
}
78 changes: 78 additions & 0 deletions frontend/src/components/DocSegment.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
8 changes: 7 additions & 1 deletion frontend/src/components/DocSegment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const props = defineProps<{
editable?: boolean
disabled?: boolean
approved?: boolean
repetitionsCount?: number
}>()

const emit = defineEmits<{
Expand Down Expand Up @@ -67,8 +68,13 @@ watchEffect(() => {

<template>
<div class="flex flex-row gap-2 font-text">
<div class="p-2 text-center w-16">
<div class="p-2 text-center w-16 relative">
{{ id }}
<div
v-if="editable && repetitionsCount && repetitionsCount > 1"
class="absolute top-1 right-1 w-2 h-2 bg-orange-500 rounded-full"
:title="`Repeated ${repetitionsCount} times`"
/>
</div>
<div
class="border rounded-border border-surface p-2 w-1/2 bg-white"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/stores/current_document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const useCurrentDocStore = defineStore('current_document', {
this.records[idx] = {
...newRecord,
loading: false,
repetitions_count: this.records[idx].repetitions_count,
}
// rerequest a document to update its records count
// this is because more than one record can be updated by a backend
Expand Down
1 change: 1 addition & 0 deletions frontend/src/views/DocView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ onMounted(async () => {
:disabled="record.loading"
:focused-id="store.currentFocusId"
:approved="record.approved"
:repetitions-count="record.repetitions_count"
@commit="(text) => onSegmentCommit(record.id, text)"
@update-record="(text) => onSegmentUpdate(record.id, text, false)"
@focus="store.focusSegment(idx)"
Expand Down