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
19 changes: 19 additions & 0 deletions backend/app/routers/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,25 @@ def download_doc(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))


@router.get(
"/{doc_id}/download_original",
response_class=StreamingResponse,
responses={
200: {
"description": "Successful Response",
"content": {"application/octet-stream": {"schema": {"type": "string"}}},
}
},
)
def download_original_doc(
doc_id: int, service: Annotated[DocumentService, Depends(get_service)]
):
try:
return service.download_original_document(doc_id)
except EntityNotFound as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))


@router.put("/{doc_id}")
def update_document(
doc_id: int,
Expand Down
36 changes: 36 additions & 0 deletions backend/app/services/document_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dataclasses import dataclass
from datetime import datetime, timedelta
from io import BytesIO

from fastapi import UploadFile
from fastapi.responses import StreamingResponse
Expand Down Expand Up @@ -246,6 +247,41 @@ def download_document(self, doc_id: int) -> StreamingResponse:

raise EntityNotFound("Unknown document type")

def download_original_document(self, doc_id: int) -> StreamingResponse:
"""
Download original document.

Args:
doc_id: Document ID

Returns:
StreamingResponse with document file

Raises:
EntityNotFound: If document not found or file not available
"""
doc = self._get_document_by_id(doc_id)
if doc.type == DocumentType.xliff:
if not doc.xliff:
raise EntityNotFound("No XLIFF file found")
original_document = doc.xliff.original_document.encode("utf-8")
output = BytesIO(original_document)
elif doc.type == DocumentType.txt:
if not doc.txt:
raise EntityNotFound("No TXT file found")
original_document = doc.txt.original_document
output = BytesIO(original_document.encode())
else:
raise EntityNotFound("Unknown document type")

return StreamingResponse(
output,
media_type="application/octet-stream",
headers={
"Content-Disposition": f'attachment; filename="{self.encode_to_latin_1(doc.name)}"'
},
)

def get_document_records(
self,
doc_id: int,
Expand Down
32 changes: 32 additions & 0 deletions backend/tests/routers/test_routes_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,3 +1233,35 @@ def test_update_document_to_same_project(
updated_doc = s.query(Document).filter_by(id=doc.id).first()
assert updated_doc is not None
assert updated_doc.project_id == project_id


def test_download_original_xliff_doc(user_logged_client: TestClient, session: Session):
"""Test downloading original XLIFF document."""
with open("tests/fixtures/small.xliff", "rb") as fp:
user_logged_client.post("/document/", files={"file": fp})

response = user_logged_client.get("/document/1/download_original")
assert response.status_code == 200

data = response.read().decode("utf-8")
assert data.startswith("<?xml version=")
assert "Regional Effects" in data


def test_download_original_txt_doc(user_logged_client: TestClient, session: Session):
"""Test downloading original TXT document."""
with open("tests/fixtures/small.txt", "rb") as fp:
user_logged_client.post("/document/", files={"file": fp})

response = user_logged_client.get("/document/1/download_original")
assert response.status_code == 200

data = response.read().decode("utf-8")
assert data.startswith("Soon after the characters enter Camp")
assert "The sloth is named Razak" in data


def test_download_original_shows_404_for_unknown_doc(user_logged_client: TestClient):
"""Test 404 when downloading original for non-existent document."""
response = user_logged_client.get("/document/1/download_original")
assert response.status_code == 404
1 change: 1 addition & 0 deletions frontend/mocks/documentMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ const docs: DocumentWithRecordsCount[] = [
name: 'Some document',
status: 'done' as DocumentStatus,
type: 'XLIFF',
project_id: 1,
},
]

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/client/services/DocumentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@ export const processDoc = async (doc_id: number, content: DocumentProcessingSett
export const getDownloadDocLink = (doc_id: number): string => {
return getApiBase() + `/document/${doc_id}/download`
}
export const getDownloadOriginalDocLink = (doc_id: number): string => {
return getApiBase() + `/document/${doc_id}/download_original`
}
10 changes: 4 additions & 6 deletions frontend/src/components/DocSegment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,33 +123,31 @@ const showHistory = () => {
</div>
<div
v-if="editable"
class="flex flex-row text-center self-start gap-2 pr-2 h-full"
class="flex flex-row text-center self-start gap-2 pr-4 h-full"
>
<Button
class="ml-auto"
:icon="icon"
rounded
outlined
variant="text"
:severity="hasComments ? 'help' : 'secondary'"
size="small"
title="View comments"
@click="showCommentsDialog"
/>
<Button
icon="pi pi-history"
rounded
outlined
variant="text"
severity="secondary"
size="small"
title="View History"
title="View history"
@click="showHistory"
/>
<Button
icon="pi pi-check"
rounded
:severity="approved ? 'success' : 'secondary'"
size="small"
title="Approve"
@click="commitData"
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/document/ToolsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const openTermModal = () => {
</script>

<template>
<div class="mx-4 mt-4 p-4 bg-surface-50 rounded-lg border border-surface">
<div class="p-4 bg-surface-50 rounded-lg border border-surface">
<div class="flex flex-row gap-4 items-center">
<InputText
v-model="sourceFilter"
Expand Down
61 changes: 35 additions & 26 deletions frontend/src/views/DocView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import {computed, ref, triggerRef, watch} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {useQuery, useQueryCache} from '@pinia/colada'

import Paginator, {PageState} from 'primevue/paginator'
import ProgressBar from 'primevue/progressbar'
import {Button, ProgressBar, Paginator, PageState} from 'primevue'

import Link from '../components/NavLink.vue'
import DocSegment from '../components/DocSegment.vue'
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 ToolsPanel from '../components/document/ToolsPanel.vue'
import TmSearchModal from '../components/TmSearchModal.vue'
Expand All @@ -22,6 +20,7 @@ import {
getDoc,
getDocRecords,
getDownloadDocLink,
getDownloadOriginalDocLink,
} from '../client/services/DocumentService'
import {updateDocRecord} from '../client/services/RecordsService'

Expand Down Expand Up @@ -68,9 +67,10 @@ const documentReady = computed(() => {
return doc.status == 'done' || doc.status == 'error'
})

const documentDownloadLink = computed(() => {
return getDownloadDocLink(documentId.value)
})
const downloadLink = computed(() => getDownloadDocLink(documentId.value))
const originalLink = computed(() =>
getDownloadOriginalDocLink(documentId.value)
)

const translationProgress = computed(() => {
const doc = document.value
Expand Down Expand Up @@ -219,6 +219,15 @@ const onShowHistory = (recordId: number) => {
historyRecordId.value = recordId
showHistoryModal.value = true
}

const percentage = computed(() =>
document.value !== undefined
? Number(
(document.value.approved_word_count / document.value.total_word_count) *
100
)
: 0.0
)
</script>

<template>
Expand All @@ -227,35 +236,34 @@ const onShowHistory = (recordId: number) => {
class="w-full h-screen grid grid-rows-[auto_1fr] overflow-hidden"
>
<div class="bg-surface-0 border-b border-surface">
<div>
<h2 class="text-xl font-bold my-4 ml-4 inline-block">
<div class="flex flex-row gap-2 items-center ml-4 mt-4 mb-1">
<Button
icon="pi pi-home"
severity="secondary"
size="small"
@click="router.push('/')"
/>

<h2 class="text-xl font-bold">
{{ document?.name }}
</h2>
<RoutingLink
name="home"
class="ml-4"
title="Return to main page"
/>
</div>
<div class="ml-4 flex flex-row gap-2 items-baseline">
Progress:
<ProgressBar
class="w-64 h-2 inline-block"
class="w-64 h-3 inline-block mx-2"
:value="translationProgress"
:show-value="false"
/>
{{ document?.approved_word_count }} /
{{ document?.total_word_count }} words <span class="text-gray-500">({{
document !== undefined
? Number(
document.approved_word_count / document.total_word_count * 100
).toFixed(2)
: 0.0
}}%)</span>
{{ document?.total_word_count }} words
<span class="text-gray-500">({{ percentage.toFixed(2) }}%)</span>
<Link
:href="downloadLink"
class="inline-block"
title="Download current file"
/>
<Link
:href="documentDownloadLink"
:href="originalLink"
class="inline-block"
title="Download in the current state"
title="Download original file"
/>
</div>
<template v-if="documentReady && !documentLoading">
Expand All @@ -266,6 +274,7 @@ const onShowHistory = (recordId: number) => {
</template>

<ToolsPanel
class="mx-4 mt-2"
@source-filter-update="(val) => (sourceFilter = val)"
@target-filter-update="(val) => (targetFilter = val)"
@open-tm-search="showTmSearchModal = true"
Expand Down