From 189d2563a90c3b8e9195c863eb09266690f7aad1 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 12 May 2025 08:56:41 -0700 Subject: [PATCH 001/208] Search across lang code too Requested DOC feature --- frontend/src/routes/languages/+page.svelte | 4 ++-- frontend/src/routes/passages/language/+page.svelte | 4 ++-- frontend/src/routes/stet/source_languages/+page.svelte | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/routes/languages/+page.svelte b/frontend/src/routes/languages/+page.svelte index 753d5328..bb441e78 100644 --- a/frontend/src/routes/languages/+page.svelte +++ b/frontend/src/routes/languages/+page.svelte @@ -181,7 +181,7 @@ $: { if (gatewayCodesAndNames) { filteredGatewayCodeAndNames = gatewayCodesAndNames.filter((item: string) => - getName(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) + getName(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) || getCode(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) ) } } @@ -192,7 +192,7 @@ $: { if (heartCodesAndNames) { filteredHeartCodeAndNames = heartCodesAndNames.filter((item: string) => - getName(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) + getName(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) || getCode(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) ) } } diff --git a/frontend/src/routes/passages/language/+page.svelte b/frontend/src/routes/passages/language/+page.svelte index 9a2be7a2..e81983f3 100644 --- a/frontend/src/routes/passages/language/+page.svelte +++ b/frontend/src/routes/passages/language/+page.svelte @@ -94,7 +94,7 @@ $: { if (gatewayCodesAndNames) { filteredGatewayCodeAndNames = gatewayCodesAndNames.filter((item: string) => - getName(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) + getName(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) || getCode(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) ) } } @@ -105,7 +105,7 @@ $: { if (heartCodesAndNames) { filteredHeartCodeAndNames = heartCodesAndNames.filter((item: string) => - getName(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) + getName(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) || getCode(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) ) } } diff --git a/frontend/src/routes/stet/source_languages/+page.svelte b/frontend/src/routes/stet/source_languages/+page.svelte index d3ea901e..7acbdcdf 100644 --- a/frontend/src/routes/stet/source_languages/+page.svelte +++ b/frontend/src/routes/stet/source_languages/+page.svelte @@ -99,7 +99,7 @@ $: { if (gatewayCodesAndNames) { filteredGatewayCodeAndNames = gatewayCodesAndNames.filter((item: string) => - getName(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) + getName(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) || getCode(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) ) } } @@ -110,7 +110,7 @@ $: { if (heartCodesAndNames) { filteredHeartCodeAndNames = heartCodesAndNames.filter((item: string) => - getName(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) + getName(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) || getCode(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) ) } } From 642d49cc351cc779df4324802bea634185c6b09f Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 12 May 2025 08:57:28 -0700 Subject: [PATCH 002/208] Update search field placeholder to clarify Make it clear that when the Gateway tab is selected, the search field searches gateway languages and when the Heart tab is selected, the search field searches heart languages. Before this commit it only said search languages which could lead one to believe you are searching across all languages. --- frontend/src/lib/LanguageSearch.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/LanguageSearch.svelte b/frontend/src/lib/LanguageSearch.svelte index 7548ffd7..8cce4645 100644 --- a/frontend/src/lib/LanguageSearch.svelte +++ b/frontend/src/lib/LanguageSearch.svelte @@ -27,7 +27,7 @@ id="filter-gl-langs" type="search" bind:value={gatewaySearchTerm} - placeholder="Search Languages" + placeholder="Search Gateway Languages" class="search-style" /> @@ -152,7 +152,7 @@ type="search" bind:value={heartSearchTerm} class="search-style" - placeholder="Search Languages" + placeholder="Search Heart Languages" /> - + + {getResourceTypeCode(lang1ResourceTypeAndName)} + + + {/each} {/if} {:else}
{#if $langCountStore > 0} -
+
1 ? 'w-1/2' : 'w-full'}>

{$langNamesStore[0]}

{/if} {#if $langCountStore > 1 && lang1ResourceTypesAndNames} -
+
1 ? 'w-1/2' : 'w-full'}>

{$langNamesStore[1]}

{/if}
{#if lang0ResourceTypesAndNames && lang0ResourceTypesAndNames.length > 0} -
+
1 ? 'w-1/2' : 'w-full'}>
Select all
-
    - {#each lang0ResourceTypesAndNames as lang0ResourceTypeAndName, index} - - {/each} -
+ {getResourceTypeName(lang0ResourceTypeAndName)} +
+ {getResourceTypeCode(lang0ResourceTypeAndName)} +
+ + {/each}
{/if} {#if lang1ResourceTypesAndNames && lang1ResourceTypesAndNames.length > 0} -
+
1 ? 'ml-4 w-1/2' : 'ml-4 w-full'}>
Select all
-
    - {#each lang1ResourceTypesAndNames as lang1ResourceTypeAndName, index} - - {/each} -
+
+ {getResourceTypeCode(lang1ResourceTypeAndName)} +
+ + {/each}
{/if}
diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 0fc163d7..6d16a6df 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -10,16 +10,16 @@ assemblyStrategyChunkSizeStore, docTypeStore, emailStore, - limitTwStore, documentRequestKeyStore, settingsUpdated, - twResourceRequestedStore, useChapterLabelsStore } from '$lib/stores/SettingsStore' import { documentReadyStore, errorStore } from '$lib/stores/NotificationStore' import { + limitTwStore, resourceTypesStore, resourceTypesCountStore, + twResourceRequestedStore, usfmAvailableStore } from '$lib/stores/ResourceTypesStore' import { langCodesStore, langCountStore } from '$lib/stores/LanguagesStore' @@ -36,22 +36,6 @@ // Set default value of chapter $assemblyStrategyChunkSizeStore = chapter.id - // Set whether TW has been requested for any of the languages - // requested so that we can use this fact in the UI to trigger the - // presence or absence of the toggle to limit TW words. - let twRegexp = new RegExp('.*tw.*') - $: { - if ($resourceTypesStore) { - $twResourceRequestedStore = $resourceTypesStore.some((item) => twRegexp.test(item)) - } - } - $: { - if ($twResourceRequestedStore && $usfmAvailableStore) { - $limitTwStore = true - } else { - $limitTwStore = false - } - } // The 3rd party HTML to PDF conversion library we use, weasyprint, // doesn't seem to be able to handle line length for the Khmer language diff --git a/frontend/src/routes/settings/GenerateDocument.svelte b/frontend/src/routes/settings/GenerateDocument.svelte index d912c727..2ecba2c3 100644 --- a/frontend/src/routes/settings/GenerateDocument.svelte +++ b/frontend/src/routes/settings/GenerateDocument.svelte @@ -4,7 +4,7 @@ import { documentReadyStore, errorStore } from '$lib/stores/NotificationStore' import { langCodesStore, langCountStore } from '$lib/stores/LanguagesStore' import { otBookStore, ntBookStore, bookCountStore } from '$lib/stores/BooksStore' - import { resourceTypesStore, resourceTypesCountStore } from '$lib/stores/ResourceTypesStore' + import { limitTwStore, resourceTypesStore, resourceTypesCountStore } from '$lib/stores/ResourceTypesStore' import { layoutForPrintStore, assemblyStrategyKindStore, @@ -14,7 +14,6 @@ generateDocxStore, emailStore, documentRequestKeyStore, - limitTwStore, settingsUpdated, useChapterLabelsStore } from '$lib/stores/SettingsStore' diff --git a/frontend/src/routes/stet/source_languages/+page.svelte b/frontend/src/routes/stet/source_languages/+page.svelte index 10d47033..68ea6653 100644 --- a/frontend/src/routes/stet/source_languages/+page.svelte +++ b/frontend/src/routes/stet/source_languages/+page.svelte @@ -1,4 +1,5 @@ diff --git a/frontend/src/lib/BlueSquareIcon.svelte b/frontend/src/lib/BlueSquareIcon.svelte new file mode 100644 index 00000000..92e34a40 --- /dev/null +++ b/frontend/src/lib/BlueSquareIcon.svelte @@ -0,0 +1,7 @@ + + + + diff --git a/frontend/src/lib/BlueSquareWithWhiteFillIcon.svelte b/frontend/src/lib/BlueSquareWithWhiteFillIcon.svelte new file mode 100644 index 00000000..baa4f772 --- /dev/null +++ b/frontend/src/lib/BlueSquareWithWhiteFillIcon.svelte @@ -0,0 +1,7 @@ + + + + diff --git a/frontend/src/lib/BookBasket.svelte b/frontend/src/lib/BookBasket.svelte new file mode 100644 index 00000000..74bd0e35 --- /dev/null +++ b/frontend/src/lib/BookBasket.svelte @@ -0,0 +1,128 @@ + + +{#if !bookRegExp.test($page.url.pathname) && $bookCountStore > 0} +
+
+ +

Book

+
+ +
+{:else} +
+ +

Book

+
+{/if} +{#if $bookCountStore > 0} + {#each shownBooks as bookCodeAndName} + {#if bookRegExp.test($page.url.pathname)} +
+
+ {getName(bookCodeAndName)}({getCode(bookCodeAndName)}) +
+ +
+ {:else} +
+
+ {getName(bookCodeAndName)}({getCode(bookCodeAndName)}) +
+
+ {/if} + {/each} + + {#if hiddenBooks.length > 0} +
+ +
+ ({hiddenBooks.length}) items hidden +
+
+ {#each hiddenBooks as bookCodeAndName} + {#if bookRegExp.test($page.url.pathname)} +
+
+ {getName(bookCodeAndName)}({getCode(bookCodeAndName)}) +
+ +
+ {:else} +
+
+ {getName(bookCodeAndName)}({getCode(bookCodeAndName)}) +
+
+ {/if} + {/each} +
+
+ {/if} +{:else} +
+ Selections will appear here once a book is selected +
+{/if} diff --git a/frontend/src/lib/BookIcon.svelte b/frontend/src/lib/BookIcon.svelte new file mode 100644 index 00000000..84008b8c --- /dev/null +++ b/frontend/src/lib/BookIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/lib/CheckIcon.svelte b/frontend/src/lib/CheckIcon.svelte new file mode 100644 index 00000000..9648c571 --- /dev/null +++ b/frontend/src/lib/CheckIcon.svelte @@ -0,0 +1,7 @@ + + + + diff --git a/frontend/src/lib/CloseIcon.svelte b/frontend/src/lib/CloseIcon.svelte new file mode 100644 index 00000000..2fd35fa8 --- /dev/null +++ b/frontend/src/lib/CloseIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/lib/DesktopBreadcrumb.svelte b/frontend/src/lib/DesktopBreadcrumb.svelte index 68988857..938439dd 100644 --- a/frontend/src/lib/DesktopBreadcrumb.svelte +++ b/frontend/src/lib/DesktopBreadcrumb.svelte @@ -7,6 +7,8 @@ import { bookCountStore } from '$lib/stores/BooksStore' import { resourceTypesCountStore } from '$lib/stores/ResourceTypesStore' import { langRegExp, bookRegExp, resourceTypeRegExp, settingsRegExp } from '$lib/utils' + import LeftArrowIcon from '$lib/LeftArrowIcon.svelte' + import RightArrowIcon from '$lib/RightArrowIcon.svelte' export let turnLangStepOn: boolean export let turnBookStepOn: boolean @@ -38,18 +40,8 @@ text-xl text-[#33445c]" disabled > - - + + {/if} @@ -144,18 +136,7 @@ disabled > - - - + {/if} diff --git a/frontend/src/lib/EditIcon.svelte b/frontend/src/lib/EditIcon.svelte new file mode 100644 index 00000000..3f8c5c09 --- /dev/null +++ b/frontend/src/lib/EditIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/lib/ErrorAlertIcon.svelte b/frontend/src/lib/ErrorAlertIcon.svelte new file mode 100644 index 00000000..9825acf4 --- /dev/null +++ b/frontend/src/lib/ErrorAlertIcon.svelte @@ -0,0 +1,10 @@ + + + diff --git a/frontend/src/lib/EyeIcon.svelte b/frontend/src/lib/EyeIcon.svelte new file mode 100644 index 00000000..ca314084 --- /dev/null +++ b/frontend/src/lib/EyeIcon.svelte @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/lib/FileIcon.svelte b/frontend/src/lib/FileIcon.svelte new file mode 100644 index 00000000..ba55aa2c --- /dev/null +++ b/frontend/src/lib/FileIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/lib/GlobeIcon.svelte b/frontend/src/lib/GlobeIcon.svelte new file mode 100644 index 00000000..cdb694e7 --- /dev/null +++ b/frontend/src/lib/GlobeIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/lib/LanguageBasket.svelte b/frontend/src/lib/LanguageBasket.svelte new file mode 100644 index 00000000..366c315a --- /dev/null +++ b/frontend/src/lib/LanguageBasket.svelte @@ -0,0 +1,119 @@ + + +{#if langRegExp.test($page.url.pathname)} +
+ +

Language

+
+{:else} +
+
+ +

Language

+
+ +
+{/if} +{#if ($gatewayCodeAndNamesStore && $gatewayCodeAndNamesStore.length > 0) || ($heartCodeAndNamesStore && $heartCodeAndNamesStore.length > 0)} + {#each $gatewayCodeAndNamesStore as langCodeAndName} + {#if langRegExp.test($page.url.pathname)} +
+
+ {getName(langCodeAndName)}({getCode(langCodeAndName)}) +
+ +
+ {:else} +
+
+ {getName(langCodeAndName)}({getCode(langCodeAndName)}) +
+
+ {/if} + {/each} + {#each $heartCodeAndNamesStore as langCodeAndName} + {#if langRegExp.test($page.url.pathname)} +
+
+ {getName(langCodeAndName)}({getCode(langCodeAndName)}) +
+ +
+ {:else} +
+
+ {getName(langCodeAndName)}({getCode(langCodeAndName)}) +
+
+ {/if} + {/each} +{:else} +
+ Selections will appear here once a language is selected +
+{/if} diff --git a/frontend/src/lib/LanguageSearch.svelte b/frontend/src/lib/LanguageSearch.svelte index 4b1086f6..863c6bc4 100644 --- a/frontend/src/lib/LanguageSearch.svelte +++ b/frontend/src/lib/LanguageSearch.svelte @@ -2,6 +2,9 @@ import Modal from '$lib/Modal.svelte' import ProgressIndicator from '$lib/ProgressIndicator.svelte' import { langCountStore } from '$lib/stores/LanguagesStore' + import BlueSquareIcon from '$lib/BlueSquareIcon.svelte' + import BlueSquareWithWhiteFillIcon from '$lib/BlueSquareWithWhiteFillIcon.svelte' + import CheckIcon from '$lib/CheckIcon.svelte' export let langCodeNameAndTypes: Array<[string, string, boolean]> export let showGatewayLanguages: boolean @@ -50,50 +53,14 @@
{/if} @@ -74,18 +65,7 @@ border border-[#E5E8EB] bg-white px-4 py-2 text-xl text-[#33445c]" disabled > - - - + {/if}
diff --git a/frontend/src/lib/Modal.svelte b/frontend/src/lib/Modal.svelte index 31c82e61..45a7d0b1 100644 --- a/frontend/src/lib/Modal.svelte +++ b/frontend/src/lib/Modal.svelte @@ -1,4 +1,6 @@ + +{#if !resourceTypeRegExp.test($page.url.pathname) && $resourceTypesCountStore > 0} +
+
+ +

Resource

+
+ +
+{:else} +
+ +

Resource

+
+{/if} +{#if $resourceTypesCountStore > 0} + {#each $resourceTypesStore as resourceTypeCodeAndName} + {#if resourceTypeRegExp.test($page.url.pathname)} +
+ {getResourceTypeName(resourceTypeCodeAndName)} ({getResourceTypeLangCode( + resourceTypeCodeAndName + )}) + +
+ {:else} +
+ {getResourceTypeName(resourceTypeCodeAndName)} ({getResourceTypeLangCode( + resourceTypeCodeAndName + )}) +
+ {/if} + {/each} +{:else} +
+ Selections will appear here once a resource is selected +
+{/if} diff --git a/frontend/src/lib/RightArrowIcon.svelte b/frontend/src/lib/RightArrowIcon.svelte new file mode 100644 index 00000000..ee905bca --- /dev/null +++ b/frontend/src/lib/RightArrowIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/lib/RightArrowWithWhiteFillIcon.svelte b/frontend/src/lib/RightArrowWithWhiteFillIcon.svelte new file mode 100644 index 00000000..0e3fc4e1 --- /dev/null +++ b/frontend/src/lib/RightArrowWithWhiteFillIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/lib/WizardBasket.svelte b/frontend/src/lib/WizardBasket.svelte index 3a8836cd..c24e6d77 100644 --- a/frontend/src/lib/WizardBasket.svelte +++ b/frontend/src/lib/WizardBasket.svelte @@ -1,459 +1,12 @@

Your Selections

- {#if langRegExp.test($page.url.pathname)} -
- - - -

Language

-
- {:else} -
-
- - - -

Language

-
- -
- {/if} - {#if ($gatewayCodeAndNamesStore && $gatewayCodeAndNamesStore.length > 0) || ($heartCodeAndNamesStore && $heartCodeAndNamesStore.length > 0)} - {#each $gatewayCodeAndNamesStore as langCodeAndName} - {#if langRegExp.test($page.url.pathname)} -
-
- {getName(langCodeAndName)}({getCode(langCodeAndName)}) -
- -
- {:else} -
-
- {getName(langCodeAndName)}({getCode(langCodeAndName)}) -
-
- {/if} - {/each} - {#each $heartCodeAndNamesStore as langCodeAndName} - {#if langRegExp.test($page.url.pathname)} -
-
- {getName(langCodeAndName)}({getCode(langCodeAndName)}) -
- -
- {:else} -
-
- {getName(langCodeAndName)}({getCode(langCodeAndName)}) -
-
- {/if} - {/each} - {:else} -
- Selections will appear here once a language is selected -
- {/if} - {#if !bookRegExp.test($page.url.pathname) && $bookCountStore > 0} -
-
- - - -

Book

-
- -
- {:else} -
- - - -

Book

-
- {/if} - {#if $bookCountStore > 0} - {#each shownBooks as bookCodeAndName} - {#if bookRegExp.test($page.url.pathname)} -
-
- {getName(bookCodeAndName)}({getCode(bookCodeAndName)}) -
- -
- {:else} -
-
- {getName(bookCodeAndName)}({getCode(bookCodeAndName)}) -
-
- {/if} - {/each} - - {#if hiddenBooks.length > 0} -
- -
- ({hiddenBooks.length}) items hidden -
-
- {#each hiddenBooks as bookCodeAndName} - {#if bookRegExp.test($page.url.pathname)} -
-
- {getName(bookCodeAndName)}({getCode(bookCodeAndName)}) -
- -
- {:else} -
-
- {getName(bookCodeAndName)}({getCode(bookCodeAndName)}) -
-
- {/if} - {/each} -
-
- {/if} - {:else} -
- Selections will appear here once a book is selected -
- {/if} - {#if !resourceTypeRegExp.test($page.url.pathname) && $resourceTypesCountStore > 0} -
-
- - - -

Resource

-
- -
- {:else} -
- - - -

Resource

-
- {/if} - {#if $resourceTypesCountStore > 0} - {#each $resourceTypesStore as resourceTypeCodeAndName} - {#if resourceTypeRegExp.test($page.url.pathname)} -
- {getResourceTypeName(resourceTypeCodeAndName)} ({getResourceTypeLangCode( - resourceTypeCodeAndName - )}) - -
- {:else} -
- {getResourceTypeName(resourceTypeCodeAndName)} ({getResourceTypeLangCode( - resourceTypeCodeAndName - )}) -
- {/if} - {/each} - {:else} -
- Selections will appear here once a resource is selected -
- {/if} + + +
diff --git a/frontend/src/lib/WizardBasketModal.svelte b/frontend/src/lib/WizardBasketModal.svelte index 7abfe851..4b56af42 100644 --- a/frontend/src/lib/WizardBasketModal.svelte +++ b/frontend/src/lib/WizardBasketModal.svelte @@ -1,4 +1,6 @@ + +{#if langRegExp.test($page.url.pathname)} +
+ +

Language

+
+{:else} +
+
+ +

Language

+
+ +
+{/if} +{#if $langCodeAndNameStore} + {#if langRegExp.test($page.url.pathname)} +
+
+ {getName($langCodeAndNameStore)}({getCode($langCodeAndNameStore)}) +
+ +
+ {:else} +
+
+ {getName($langCodeAndNameStore)}({getCode($langCodeAndNameStore)}) +
+
+ {/if} +{:else} +
+ Language will appear here selected +
+{/if} diff --git a/frontend/src/lib/passages/MobileBreadcrumb.svelte b/frontend/src/lib/passages/MobileBreadcrumb.svelte index ae5bd1ed..b28acd32 100644 --- a/frontend/src/lib/passages/MobileBreadcrumb.svelte +++ b/frontend/src/lib/passages/MobileBreadcrumb.svelte @@ -6,6 +6,8 @@ import { langCodeAndNameStore } from '$lib/passages/stores/LanguageStore' import { passagesStore } from '$lib/passages/stores/PassagesStore' import { getCode, langRegExp, passagesRegExp, settingsRegExp } from '$lib/passages/utils' + import LeftArrowIcon from '$lib/LeftArrowIcon.svelte' + import RightArrowIcon from '$lib/RightArrowIcon.svelte' export let title: string export let stepLabel: string @@ -38,18 +40,7 @@ text-xl text-[#33445c]" disabled > - - + {/if} @@ -63,18 +54,7 @@ border border-[#E5E8EB] bg-white px-4 py-2 text-xl text-[#33445c]" disabled > - - - + {/if} diff --git a/frontend/src/lib/passages/PassagesBasket.svelte b/frontend/src/lib/passages/PassagesBasket.svelte new file mode 100644 index 00000000..7fdb2d52 --- /dev/null +++ b/frontend/src/lib/passages/PassagesBasket.svelte @@ -0,0 +1,152 @@ + + +{#if passagesRegExp.test($page.url.pathname)} +
+ +

Passages

+
+{:else} +
+
+ +

Passages

+
+ +
+{/if} + +{#if $passagesStore && $passagesStore.length > 0} + {#each shownPassages as passage} + {#if passagesRegExp.test($page.url.pathname)} +
+
+ {#if passage.endChapterNum != null && passage.endChapterNum > 0 && passage.endChapterVerseReference != null} + {passage.bookName} + {passage.startChapterNum}:{passage.startChapterVerseReference}-{passage.endChapterNum}:{passage.endChapterVerseReference} + {:else} + {passage.bookName} + {passage.startChapterNum}:{passage.startChapterVerseReference} + {/if} +
+ +
+ {:else} +
+
+ {#if passage.endChapterNum != null && passage.endChapterNum > 0 && passage.endChapterVerseReference != null} + {passage.bookName} + {passage.startChapterNum}:{passage.startChapterVerseReference}-{passage.endChapterNum}:{passage.endChapterVerseReference} + {:else} + {passage.bookName} + {passage.startChapterNum}:{passage.startChapterVerseReference} + {/if} +
+
+ {/if} + {/each} + + {#if hiddenPassages.length > 0} +
+ +
+ ({hiddenPassages.length}) items hidden +
+
+ {#each hiddenPassages as passage} + {#if passagesRegExp.test($page.url.pathname)} +
+
+ {#if passage.endChapterNum != null && passage.endChapterNum > 0 && passage.endChapterVerseReference != null} + {passage.bookName} + {passage.startChapterNum}:{passage.startChapterVerseReference}-{passage.endChapterNum}:{passage.endChapterVerseReference} + {:else} + {passage.bookName} + {passage.startChapterNum}:{passage.startChapterVerseReference} + {/if} +
+ +
+ {:else} +
+
+ {#if passage.endChapterNum != null && passage.endChapterNum > 0 && passage.endChapterVerseReference != null} + {passage.bookName} + {passage.startChapterNum}:{passage.startChapterVerseReference}-{passage.endChapterNum}:{passage.endChapterVerseReference} + {:else} + {passage.bookName} + {passage.startChapterNum}:{passage.startChapterVerseReference} + {/if} +
+
+ {/if} + {/each} +
+
+ {/if} +{:else} +
+ Selections will appear here once a passage is added +
+{/if} diff --git a/frontend/src/lib/passages/WizardBasket.svelte b/frontend/src/lib/passages/WizardBasket.svelte index 6a55518a..d379e341 100644 --- a/frontend/src/lib/passages/WizardBasket.svelte +++ b/frontend/src/lib/passages/WizardBasket.svelte @@ -1,262 +1,10 @@

Your Selections

- {#if langRegExp.test($page.url.pathname)} -
- - - -

Language

-
- {:else} -
-
- - - -

Language

-
- -
- {/if} - {#if $langCodeAndNameStore} - {#if langRegExp.test($page.url.pathname)} -
-
- {getName($langCodeAndNameStore)}({getCode($langCodeAndNameStore)}) -
- -
- {:else} -
-
- {getName($langCodeAndNameStore)}({getCode($langCodeAndNameStore)}) -
-
- {/if} - {:else} -
- Language will appear here selected -
- {/if} - {#if passagesRegExp.test($page.url.pathname)} -
- - - -

Passages

-
- {:else} -
-
- - - -

Passages

-
- -
- {/if} - {#if $passagesStore} - {#if passagesRegExp.test($page.url.pathname)} - {#each $passagesStore as passage} -
-
- {#if passage.endChapterNum != null && passage.endChapterNum > 0 && passage.endChapterVerseReference != null} - {passage.bookName} - {passage.startChapterNum}:{passage.startChapterVerseReference}-{passage.endChapterNum}:{passage.endChapterVerseReference} - {:else} - {passage.bookName} - {passage.startChapterNum}:{passage.startChapterVerseReference} - {/if} -
- -
- {/each} - {:else} - {#each $passagesStore as passage} -
-
- {#if passage.endChapterNum != null && passage.endChapterNum > 0 && passage.endChapterVerseReference != null} - {passage.bookName} - {passage.startChapterNum}:{passage.startChapterVerseReference}-{passage.endChapterNum}:{passage.endChapterVerseReference} - {:else} - {passage.bookName} - {passage.startChapterNum}:{passage.startChapterVerseReference} - {/if} -
-
- {/each} - {/if} - {:else} -
- Selections will appear here once a passage is added -
- {/if} + +
diff --git a/frontend/src/lib/stet/DesktopBreadcrumb.svelte b/frontend/src/lib/stet/DesktopBreadcrumb.svelte index d8a3c324..9f6cd032 100644 --- a/frontend/src/lib/stet/DesktopBreadcrumb.svelte +++ b/frontend/src/lib/stet/DesktopBreadcrumb.svelte @@ -10,6 +10,8 @@ langCountStore } from '$lib/stet/stores/LanguagesStore' import { getCode, sourceLangRegExp, targetLangRegExp, settingsRegExp } from '$lib/stet/utils' + import LeftArrowIcon from '$lib/LeftArrowIcon.svelte' + import RightArrowIcon from '$lib/RightArrowIcon.svelte' export let turnSourceLangStepOn: boolean export let turnTargetLangStepOn: boolean @@ -35,18 +37,8 @@ text-xl text-[#33445c]" disabled > - - + + {/if} @@ -128,18 +120,7 @@ disabled > - - - + {/if} diff --git a/frontend/src/lib/stet/MobileBreadcrumb.svelte b/frontend/src/lib/stet/MobileBreadcrumb.svelte index 749bc7a0..28b63dfa 100644 --- a/frontend/src/lib/stet/MobileBreadcrumb.svelte +++ b/frontend/src/lib/stet/MobileBreadcrumb.svelte @@ -9,6 +9,8 @@ langCountStore } from '$lib/stet/stores/LanguagesStore' import { getCode, sourceLangRegExp, targetLangRegExp, settingsRegExp } from '$lib/stet/utils' + import LeftArrowIcon from '$lib/LeftArrowIcon.svelte' + import RightArrowIcon from '$lib/RightArrowIcon.svelte' export let title: string export let stepLabel: string @@ -43,18 +45,7 @@ text-xl text-[#33445c]" disabled > - - + {/if} @@ -68,18 +59,7 @@ border border-[#E5E8EB] bg-white px-4 py-2 text-xl text-[#33445c]" disabled > - - - + {/if} diff --git a/frontend/src/lib/stet/SourceLanguageBasket.svelte b/frontend/src/lib/stet/SourceLanguageBasket.svelte new file mode 100644 index 00000000..caea3756 --- /dev/null +++ b/frontend/src/lib/stet/SourceLanguageBasket.svelte @@ -0,0 +1,112 @@ + + +{#if sourceLangRegExp.test($page.url.pathname)} +
+ +

Source Language

+
+{:else} +
+
+ +

Source Language

+
+ +
+{/if} +{#if $lang0CodeAndNameStore} + {#if sourceLangRegExp.test($page.url.pathname)} +
+
+ {getName($lang0CodeAndNameStore)}({getCode($lang0CodeAndNameStore)}) +
+ +
+ {:else} +
+
+ {getName($lang0CodeAndNameStore)}({getCode($lang0CodeAndNameStore)}) +
+
+ {/if} +{:else} +
+ Selections will appear here once a source language is selected +
+{/if} diff --git a/frontend/src/lib/stet/WizardBasket.svelte b/frontend/src/lib/stet/WizardBasket.svelte index 7c2f6ae0..7effb1be 100644 --- a/frontend/src/lib/stet/WizardBasket.svelte +++ b/frontend/src/lib/stet/WizardBasket.svelte @@ -10,6 +10,10 @@ heartCodeAndNamesStore } from '$lib/stet/stores/LanguagesStore' import { sourceLangRegExp, targetLangRegExp, getCode, getName } from '$lib/stet/utils' + import SourceLanguageBasket from './SourceLanguageBasket.svelte' + import GlobeIcon from '$lib/GlobeIcon.svelte' + import EditIcon from '$lib/EditIcon.svelte' + import CloseIcon from '$lib/CloseIcon.svelte' function uncheckGatewayLanguage(langCodeAndName: string) { if (langCodeAndName) { @@ -52,138 +56,16 @@

Your Selections

- {#if sourceLangRegExp.test($page.url.pathname)} -
- - - -

Source Language

-
- {:else} -
-
- - - -

Source Language

-
- -
- {/if} - {#if $lang0CodeAndNameStore} - {#if sourceLangRegExp.test($page.url.pathname)} -
-
- {getName($lang0CodeAndNameStore)}({getCode($lang0CodeAndNameStore)}) -
- -
- {:else} -
-
- {getName($lang0CodeAndNameStore)}({getCode($lang0CodeAndNameStore)}) -
-
- {/if} - {:else} -
- Selections will appear here once a source language is selected -
- {/if} + {#if targetLangRegExp.test($page.url.pathname)}
- - - +

Target Language

{:else}
- - - +

Target Language

@@ -226,18 +97,7 @@ uncheckHeartLanguage($lang1CodeAndNameStore) }} > - - - +
{:else} diff --git a/frontend/src/routes/books/+page.svelte b/frontend/src/routes/books/+page.svelte index b242b6ba..b7890eb5 100644 --- a/frontend/src/routes/books/+page.svelte +++ b/frontend/src/routes/books/+page.svelte @@ -18,6 +18,10 @@ import Modal from '$lib/Modal.svelte' import ProgressIndicator from '$lib/ProgressIndicator.svelte' import { errorStore } from '$lib/stores/NotificationStore' + import BlueSquareIcon from '$lib/BlueSquareIcon.svelte' + import CheckIcon from '$lib/CheckIcon.svelte' + import ErrorAlertIcon from '$lib/ErrorAlertIcon.svelte' + import BlueSquareWithWhiteFillIcon from '../../lib/BlueSquareWithWhiteFillIcon.svelte' async function getBookCodesAndNames( langCode0: string, @@ -161,50 +165,14 @@
diff --git a/frontend/src/routes/stet/settings/+page.svelte b/frontend/src/routes/stet/settings/+page.svelte index 8cfc7ac3..5b0cbfdd 100644 --- a/frontend/src/routes/stet/settings/+page.svelte +++ b/frontend/src/routes/stet/settings/+page.svelte @@ -27,6 +27,7 @@ // import { bookCountStore } from '$lib/stores/BooksStore' import GenerateDocument from './GenerateDocument.svelte' import LogRocket from 'logrocket' + import CheckIcon from '$lib/CheckIcon.svelte' // let chapter: SelectElement = { // id: 'chapter', @@ -104,19 +105,7 @@
+
+ {#if passageSuccessMessage} +
+ {passageSuccessMessage} +
+ {/if} +
Date: Wed, 18 Jun 2025 15:14:17 -0700 Subject: [PATCH 073/208] Better check icon (svg) --- .../passages/BibleReferenceSelector.svelte | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index f1af9de2..1bbfaa99 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -17,6 +17,8 @@ let ntSurveySuccessMessage: string = '' let stetSuccessMessage: string = '' let passageSuccessMessage: string = '' + let checkIcon = + '' let selectedBookCode: string = '' let selectedChapter: string = '' @@ -264,8 +266,10 @@
@@ -317,7 +321,7 @@
{:else if stetSuccessMessage}
- {stetSuccessMessage} + {@html checkIcon}
{/if} @@ -351,4 +355,12 @@ transform: rotate(360deg); } } + * :global(.add-passage-button) { + background: + linear-gradient(180deg, #1876fd 0%, #015ad9 100%), linear-gradient(0deg, #33445c, #33445c); + } + * :global(.add-passage-button:hover) { + background: + linear-gradient(180deg, #0765ec 0%, #0149c8 100%), linear-gradient(0deg, #33445c, #33445c); + } From e8a96a864dd86776a513b3a6bb443c4c69f306ea Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 18 Jun 2025 15:19:35 -0700 Subject: [PATCH 074/208] Remove unused import --- .../src/routes/passages/passages/BibleReferenceSelector.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index 1bbfaa99..f2c56ee7 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -6,7 +6,6 @@ PUBLIC_CHAPTERS_IN_BOOKS_URL, PUBLIC_NT_SURVEY_RG_PASSAGES_URL, PUBLIC_STET_PASSAGES_URL, - PUBLIC_TAILWIND_SM_MIN_WIDTH } from '$env/static/public' import { env } from '$env/dynamic/public' import type { BibleReference } from './model' From 7c139060cd77234796346d33f24bc62371939fe6 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 23 Jun 2025 14:21:06 -0700 Subject: [PATCH 075/208] Add env var for dotnet API port --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 926c65c4..70c4450a 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: usfmparserapi: build: ./dotnet/USFMParserApi ports: - - 8081:80 + - ${DOTNET_API_PORT:-8081}:80 volumes: - shared_assets:/app/assets_download - shared_working:/app/working_temp From f155a679c3d4a3e172929f4e00e8aee308508f57 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 26 Jun 2025 09:59:39 -0700 Subject: [PATCH 076/208] Refactor css to resolve (most) svelte css compiler warnings --- frontend/src/routes/+layout.svelte | 25 ++++++++++ frontend/src/routes/books/+page.svelte | 15 ++---- frontend/src/routes/languages/+page.svelte | 49 ------------------- frontend/src/routes/passages/+layout.svelte | 7 --- .../src/routes/passages/language/+page.svelte | 49 ------------------- .../src/routes/passages/passages/+page.svelte | 28 ----------- .../src/routes/passages/settings/+page.svelte | 12 ----- .../src/routes/resource_types/+page.svelte | 12 ----- frontend/src/routes/settings/+page.svelte | 12 ----- frontend/src/routes/stet/+layout.svelte | 6 --- .../src/routes/stet/settings/+page.svelte | 12 ----- .../routes/stet/source_languages/+page.svelte | 49 ------------------- .../[lang0_code]/+page.svelte | 28 ----------- 13 files changed, 28 insertions(+), 276 deletions(-) diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 5e9ae8c4..78e32716 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -14,4 +14,29 @@ margin-top: 12px; margin-left: 12px; } + + :global(li.target:has(input[type='checkbox']:checked)) { + background: #e6eefb; + } + :global(div.target:has(input[type='checkbox']:checked)) { + background: #e6eefb; + } + :global(div.target:has(input[type='radio']:checked)) { + background: #e6eefb; + } + :global(div.target2:has(input[type='checkbox']:checked) + div) { + color: #015ad9; + } + :global(div.target2:has(input[type='checkbox']:checked) + span) { + color: #015ad9; + } + :global(div.target3:has(input[type='checkbox']:checked) + span) { + color: #015ad9; + } + :global(input.checkbox-target[type='checkbox']:checked + span) { + color: #015ad9; + } + :global(.checkbox-style) { + @apply h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600; + } diff --git a/frontend/src/routes/books/+page.svelte b/frontend/src/routes/books/+page.svelte index 1e584151..03dc6802 100644 --- a/frontend/src/routes/books/+page.svelte +++ b/frontend/src/routes/books/+page.svelte @@ -378,24 +378,15 @@ background-position: left center; outline: 0; } - div.target:has(input[type='checkbox']:checked) { + div.radio-target:has(input[type='radio']:checked) { background: #e6eefb; } - div.target:has(input[type='radio']:checked) { - background: #e6eefb; - } - input.checkbox-target[type='checkbox']:checked + span { + input#show-gateway-radio-button[type='radio']:checked + span { color: #015ad9; } - div.target3:has(input[type='checkbox']:checked) + span { + input#show-heart-radio-button[type='radio']:checked + span { color: #015ad9; } - div.target2:has(input[type='checkbox']:checked) + div { - color: #015ad9; - } - .checkbox-style { - @apply h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600; - } .radio-style { @apply h-4 w-4 border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600; } diff --git a/frontend/src/routes/languages/+page.svelte b/frontend/src/routes/languages/+page.svelte index 0aaf7125..35429904 100644 --- a/frontend/src/routes/languages/+page.svelte +++ b/frontend/src/routes/languages/+page.svelte @@ -215,52 +215,3 @@ - - diff --git a/frontend/src/routes/passages/+layout.svelte b/frontend/src/routes/passages/+layout.svelte index d8f66ca1..9858e3f9 100644 --- a/frontend/src/routes/passages/+layout.svelte +++ b/frontend/src/routes/passages/+layout.svelte @@ -1,13 +1,6 @@ - diff --git a/frontend/src/routes/passages/language/+page.svelte b/frontend/src/routes/passages/language/+page.svelte index 0dc05061..f6c5df2d 100644 --- a/frontend/src/routes/passages/language/+page.svelte +++ b/frontend/src/routes/passages/language/+page.svelte @@ -168,52 +168,3 @@ - - diff --git a/frontend/src/routes/passages/passages/+page.svelte b/frontend/src/routes/passages/passages/+page.svelte index 4bbc58e7..707aae3a 100644 --- a/frontend/src/routes/passages/passages/+page.svelte +++ b/frontend/src/routes/passages/passages/+page.svelte @@ -125,31 +125,3 @@ - - diff --git a/frontend/src/routes/passages/settings/+page.svelte b/frontend/src/routes/passages/settings/+page.svelte index 073fa309..bf33f3f9 100644 --- a/frontend/src/routes/passages/settings/+page.svelte +++ b/frontend/src/routes/passages/settings/+page.svelte @@ -124,15 +124,3 @@ - - diff --git a/frontend/src/routes/resource_types/+page.svelte b/frontend/src/routes/resource_types/+page.svelte index 633d5ccf..03fd49a1 100644 --- a/frontend/src/routes/resource_types/+page.svelte +++ b/frontend/src/routes/resource_types/+page.svelte @@ -372,15 +372,3 @@ - - diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index d508b413..03fd5c7e 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -299,15 +299,3 @@ - - diff --git a/frontend/src/routes/stet/+layout.svelte b/frontend/src/routes/stet/+layout.svelte index d8f66ca1..cf3304ee 100644 --- a/frontend/src/routes/stet/+layout.svelte +++ b/frontend/src/routes/stet/+layout.svelte @@ -5,9 +5,3 @@ - diff --git a/frontend/src/routes/stet/settings/+page.svelte b/frontend/src/routes/stet/settings/+page.svelte index 0ef0f615..a780d0a4 100644 --- a/frontend/src/routes/stet/settings/+page.svelte +++ b/frontend/src/routes/stet/settings/+page.svelte @@ -126,15 +126,3 @@ - - diff --git a/frontend/src/routes/stet/source_languages/+page.svelte b/frontend/src/routes/stet/source_languages/+page.svelte index 68ea6653..d6a7b83b 100644 --- a/frontend/src/routes/stet/source_languages/+page.svelte +++ b/frontend/src/routes/stet/source_languages/+page.svelte @@ -203,52 +203,3 @@ - - diff --git a/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte b/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte index fa68c571..489c053a 100644 --- a/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte +++ b/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte @@ -188,31 +188,3 @@ - - From b8b26dd1e06c0493e4ec10f65d548b1610985cea Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 26 Jun 2025 10:02:47 -0700 Subject: [PATCH 077/208] Minor formatting --- .../src/routes/passages/passages/+page.svelte | 4 ---- .../passages/BibleReferenceSelector.svelte | 2 +- .../src/routes/passages/settings/+page.svelte | 5 +---- .../src/routes/stet/settings/+page.svelte | 1 - .../routes/stet/source_languages/+page.svelte | 20 +++++++++++-------- .../[lang0_code]/+page.svelte | 2 +- 6 files changed, 15 insertions(+), 19 deletions(-) diff --git a/frontend/src/routes/passages/passages/+page.svelte b/frontend/src/routes/passages/passages/+page.svelte index 707aae3a..2eaf331e 100644 --- a/frontend/src/routes/passages/passages/+page.svelte +++ b/frontend/src/routes/passages/passages/+page.svelte @@ -34,7 +34,6 @@ return bookCodesAndNames } - let bookCodesAndNames: Array<[string, string]> = [] onMount(() => { @@ -46,7 +45,6 @@ bookCodesAndNames = [...bookCodesAndNames_] // Ensure reactivity with [...blah] }) .catch((err) => console.error(err)) - }) function removePassage(id: number) { @@ -61,8 +59,6 @@ } } - - let windowWidth: number = typeof window !== 'undefined' ? window.innerWidth : 0 let TAILWIND_SM_MIN_WIDTH: number = PUBLIC_TAILWIND_SM_MIN_WIDTH as unknown as number diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index f2c56ee7..dba2eb97 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -5,7 +5,7 @@ import { PUBLIC_CHAPTERS_IN_BOOKS_URL, PUBLIC_NT_SURVEY_RG_PASSAGES_URL, - PUBLIC_STET_PASSAGES_URL, + PUBLIC_STET_PASSAGES_URL } from '$env/static/public' import { env } from '$env/dynamic/public' import type { BibleReference } from './model' diff --git a/frontend/src/routes/passages/settings/+page.svelte b/frontend/src/routes/passages/settings/+page.svelte index bf33f3f9..675274d6 100644 --- a/frontend/src/routes/passages/settings/+page.svelte +++ b/frontend/src/routes/passages/settings/+page.svelte @@ -2,10 +2,7 @@ import WizardBreadcrumb from '$lib/passages/WizardBreadcrumb.svelte' import WizardBasket from '$lib/passages/WizardBasket.svelte' import WizardBasketModal from '$lib/WizardBasketModal.svelte' - import { - emailStore, - documentRequestKeyStore, - } from '$lib/passages/stores/SettingsStore' + import { emailStore, documentRequestKeyStore } from '$lib/passages/stores/SettingsStore' import { documentReadyStore, errorStore } from '$lib/passages/stores/NotificationStore' import { passagesStore } from '$lib/passages/stores/PassagesStore' import GenerateDocument from './GenerateDocument.svelte' diff --git a/frontend/src/routes/stet/settings/+page.svelte b/frontend/src/routes/stet/settings/+page.svelte index a780d0a4..0cc63e34 100644 --- a/frontend/src/routes/stet/settings/+page.svelte +++ b/frontend/src/routes/stet/settings/+page.svelte @@ -13,7 +13,6 @@ import LogRocket from 'logrocket' import CheckIcon from '$lib/CheckIcon.svelte' - $: showEmail = false $: showEmailCaptured = false $: $documentReadyStore = false diff --git a/frontend/src/routes/stet/source_languages/+page.svelte b/frontend/src/routes/stet/source_languages/+page.svelte index d6a7b83b..6e3489ed 100644 --- a/frontend/src/routes/stet/source_languages/+page.svelte +++ b/frontend/src/routes/stet/source_languages/+page.svelte @@ -81,8 +81,8 @@ let heartCodesAndNames: Array = [] async function loadSourceLangCodesAndNames() { try { - langCodeNameAndTypes = await getSourceLangCodesNames() - gatewayCodesAndNames = langCodeNameAndTypes + langCodeNameAndTypes = await getSourceLangCodesNames() + gatewayCodesAndNames = langCodeNameAndTypes .filter((element: [string, string, boolean]) => { return element[2] }) @@ -92,7 +92,7 @@ return !element[2] }) .map((tuple) => `${tuple[0]}, ${tuple[1]}`) - } catch(err) { + } catch (err) { console.error(err) } } @@ -123,8 +123,10 @@ let filteredGatewayCodeAndNames: Array = [] $: { if (gatewayCodesAndNames) { - filteredGatewayCodeAndNames = gatewayCodesAndNames.filter((item: string) => - getName(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) || getCode(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) + filteredGatewayCodeAndNames = gatewayCodesAndNames.filter( + (item: string) => + getName(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) || + getCode(item.toLowerCase()).includes(gatewaySearchTerm.toLowerCase()) ) } } @@ -134,13 +136,15 @@ let filteredHeartCodeAndNames: Array = [] $: { if (heartCodesAndNames) { - filteredHeartCodeAndNames = heartCodesAndNames.filter((item: string) => - getName(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) || getCode(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) + filteredHeartCodeAndNames = heartCodesAndNames.filter( + (item: string) => + getName(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) || + getCode(item.toLowerCase()).includes(heartSearchTerm.toLowerCase()) ) } } - let windowWidth: number = typeof window !== "undefined" ? window.innerWidth : 0 + let windowWidth: number = typeof window !== 'undefined' ? window.innerWidth : 0 $: console.log(`windowWidth: ${windowWidth}`) let TAILWIND_SM_MIN_WIDTH: number = PUBLIC_TAILWIND_SM_MIN_WIDTH as unknown as number diff --git a/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte b/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte index 489c053a..8e43e70b 100644 --- a/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte +++ b/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte @@ -125,7 +125,7 @@ ) } - let windowWidth: number = typeof window !== "undefined" ? window.innerWidth : 0 + let windowWidth: number = typeof window !== 'undefined' ? window.innerWidth : 0 $: console.log(`windowWidth: ${windowWidth}`) let TAILWIND_SM_MIN_WIDTH: number = PUBLIC_TAILWIND_SM_MIN_WIDTH as unknown as number From 8190eba98bfe197d456fa64d4e15c86e44b2aa2e Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 26 Jun 2025 10:06:49 -0700 Subject: [PATCH 078/208] Small simplification --- .../routes/passages/passages/BibleReferenceSelector.svelte | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index dba2eb97..7f1a47df 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -64,11 +64,12 @@ export async function addNTSurveyRGPassages() { try { - const bibleReferences = await getNTSurveyRGPassages($langCodeAndNameStore.split(',')[0]) - console.log(`bibleReferences[0]: ${bibleReferences[0]}`) + langCode = $langCodeAndNameStore.split(',')[0] + const bibleReferences = await getNTSurveyRGPassages(langCode) + // console.log(`bibleReferences[0]: ${bibleReferences[0]}`) for (const bibleRef of bibleReferences) { addPassageReference( - $langCodeAndNameStore.split(',')[0], + langCode, bibleRef.book_code, bibleRef.book_name, Number(bibleRef.start_chapter), From 74b664c22ba25868aab566c8a8d1aad42b3a8803 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 26 Jun 2025 10:31:44 -0700 Subject: [PATCH 079/208] Fix compile error --- .../passages/passages/BibleReferenceSelector.svelte | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index 7f1a47df..f7571537 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -64,9 +64,9 @@ export async function addNTSurveyRGPassages() { try { - langCode = $langCodeAndNameStore.split(',')[0] + const langCode = $langCodeAndNameStore.split(',')[0] const bibleReferences = await getNTSurveyRGPassages(langCode) - // console.log(`bibleReferences[0]: ${bibleReferences[0]}`) + console.log(`bibleReferences[0]: ${bibleReferences[0]}`) for (const bibleRef of bibleReferences) { addPassageReference( langCode, @@ -103,11 +103,12 @@ export async function addSTETPassages() { try { - const bibleReferences = await getSTETPassages($langCodeAndNameStore.split(',')[0]) + const langCode = $langCodeAndNameStore.split(',')[0] + const bibleReferences = await getSTETPassages(langCode) console.log(`bibleReferences[0]: ${bibleReferences[0]}`) for (const bibleRef of bibleReferences) { addPassageReference( - $langCodeAndNameStore.split(',')[0], + langCode, bibleRef.book_code, bibleRef.book_name, Number(bibleRef.start_chapter), From 164f4bf54249fdc89020604a5c8924bd5101afca Mon Sep 17 00:00:00 2001 From: Will Kelly <67284402+wkelly17@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:00:06 -0500 Subject: [PATCH 080/208] Wk add parser image (#268) * separate usfm parser build and run --- .github/workflows/docker-publish.yml | 60 +++++++++++++++++++++++++--- docker-compose.yml | 4 +- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index d96814ea..ce329078 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -35,16 +35,31 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + Build-USFMParser-Image: + name: Build USFMParser image + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + - uses: FranzDiebold/github-env-vars-action@v2 + - uses: docker/setup-qemu-action@v2 + - uses: docker/setup-buildx-action@v2 + - name: Build USFMParser image + uses: docker/build-push-action@v3 + with: + context: ./dotnet/USFMParserApi + cache-from: type=gha + cache-to: type=gha,mode=max + Test-API-Image: name: Test API image runs-on: ubuntu-24.04 - needs: Build-API-Image + needs: [Build-API-Image, Build-USFMParser-Image] steps: - uses: actions/checkout@v3 - uses: FranzDiebold/github-env-vars-action@v2 - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - - name: Build API image with buildx + - name: Build local API image with buildx uses: docker/build-push-action@v3 with: context: . @@ -53,6 +68,15 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + - name: Build local USFMParser image with buildx + uses: docker/build-push-action@v3 + with: + context: ./dotnet/USFMParserApi + tags: wycliffeassociates/doc-usfmparser:local + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Test API image with docker-compose run: | docker compose -f docker-compose.yml -f docker-compose.api-test.yml up --exit-code-from test-runner @@ -60,13 +84,13 @@ jobs: Test-Frontend-Image: name: Test Frontend image runs-on: ubuntu-24.04 - needs: [Build-API-Image, Build-Frontend-Image] + needs: [Build-API-Image, Build-Frontend-Image, Build-USFMParser-Image] steps: - uses: actions/checkout@v3 - uses: FranzDiebold/github-env-vars-action@v2 - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - - name: Build api image + - name: Build local api image uses: docker/build-push-action@v3 with: context: . @@ -74,7 +98,7 @@ jobs: load: true cache-from: type=gha cache-to: type=gha,mode=max - - name: Build frontend image + - name: Build local frontend image uses: docker/build-push-action@v3 with: context: ./frontend @@ -82,6 +106,16 @@ jobs: load: true cache-from: type=gha cache-to: type=gha,mode=max + + - name: Build local USFMParser image with buildx + uses: docker/build-push-action@v3 + with: + context: ./dotnet/USFMParserApi + tags: wycliffeassociates/doc-usfmparser:local + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Build frontend-tests image uses: docker/build-push-action@v3 with: @@ -120,6 +154,11 @@ jobs: echo "DOC_UI_TAG_SHA=wycliffeassociates/doc-ui:$GITHUB_SHA" >> $GITHUB_ENV && \ echo "DOC_UI_TAG_BRANCH=wycliffeassociates/doc-ui:$CI_REF_NAME_SLUG" >> $GITHUB_ENV && \ echo "DOC_UI_TAG_LATEST=wycliffeassociates/doc-ui:latest" >> $GITHUB_ENV + - name: Set DOC USFM parser tags + run: | + echo "DOC_PARSER_TAG_SHA=wycliffeassociates/doc-usfmparser:$GITHUB_SHA" >> $GITHUB_ENV && \ + echo "DOC_PARSER_TAG_BRANCH=wycliffeassociates/doc-usfmparser:$CI_REF_NAME_SLUG" >> $GITHUB_ENV && \ + echo "DOC_PARSER_TAG_LATEST=wycliffeassociates/doc-usfmparser:latest" >> $GITHUB_ENV - name: Set version inside UI run: | @@ -152,6 +191,17 @@ jobs: PUBLIC_DOC_BUILD_TIMESTAMP_ARG=${{ env.PUBLIC_DOC_BUILD_TIMESTAMP }} cache-from: type=gha cache-to: type=gha,mode=max + - name: Build and conditional push Parser image + uses: docker/build-push-action@v3 + with: + context: ./dotnet/USFMParserApi + push: true + tags: | + ${{ env.DOC_PARSER_TAG_SHA }} + ${{ env.DOC_PARSER_TAG_BRANCH }} + ${{ env.DOC_PARSER_TAG_LATEST }} + cache-from: type=gha + cache-to: type=gha,mode=max Deploy-develop: name: Develop deploy on successful develop build needs: [Push-Images] diff --git a/docker-compose.yml b/docker-compose.yml index 70c4450a..a5803f77 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,9 @@ services: retries: 5 restart: unless-stopped usfmparserapi: - build: ./dotnet/USFMParserApi + image: wycliffeassociates/doc-usfmparser:${IMAGE_TAG} + environment: + IMAGE_TAG: ${IMAGE_TAG:-local} ports: - ${DOTNET_API_PORT:-8081}:80 volumes: From 4e82b3698850deef1f85c8e2f8c29177ea411b2e Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 27 Jun 2025 14:11:57 -0700 Subject: [PATCH 081/208] Add ability to download assets rather than clone Can choose to download and unzip or to clone via configuration setting --- .env | 5 ++ backend/doc/config.py | 2 + backend/doc/domain/resource_lookup.py | 79 ++++++++++++++++++++++----- backend/doc/utils/file_utils.py | 18 ++++++ 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/.env b/.env index b3d2c925..df33168f 100644 --- a/.env +++ b/.env @@ -65,6 +65,11 @@ USE_LOCALIZED_BOOK_NAME=true # in parsing.RESOURCES_WITH_USFM_DEFECTS. CHECK_ALL_BOOKS_FOR_LANGUAGE=true +# When true the system will acquire git repos via download of its +# master.zip file. When false it will clone the repo. Both approaches +# are optimized maximally by taking advantage of curl and git options. +DOWNLOAD_ASSETS=true + # * http://localhost:3000 covers requests originating from the case # where 'npm run dev' is invoked to run vite (to run svelte js frontend) # outside Docker. This results in vite's development mode which runs on diff --git a/backend/doc/config.py b/backend/doc/config.py index e47023fd..ab6a3378 100755 --- a/backend/doc/config.py +++ b/backend/doc/config.py @@ -42,6 +42,8 @@ class Settings(BaseSettings): USE_LOCALIZED_BOOK_NAME: bool CHECK_ALL_BOOKS_FOR_LANGUAGE: bool + DOWNLOAD_ASSETS: bool # If true then download assets, else clone assets + def logger(self, name: str) -> logging.Logger: """ Return a Logger for scope named by name, e.g., module, that can be diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index b1a3af31..dc02b933 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -11,7 +11,7 @@ import subprocess from glob import glob from os import scandir, stat -from os.path import basename, exists, isdir, join +from os.path import exists, isdir, join from pathlib import Path from typing import Mapping, Optional, Sequence from urllib.parse import urlparse @@ -39,7 +39,13 @@ get_rg_books, parse_bible_reference, ) -from doc.utils.file_utils import file_needs_update, make_dir, read_file +from doc.utils.file_utils import ( + delete_tree, + dir_needs_update, + file_needs_update, + make_dir, + read_file, +) from doc.utils.list_utils import unique_tuples from doc.utils.text_utils import normalize_localized_book_name from fastapi import HTTPException, status @@ -335,6 +341,7 @@ def resource_types( book_names: Mapping[str, str] = BOOK_NAMES, docx_file_path: str = "en_rg_nt_survey.docx", en_rg: str = settings.EN_RG_DIR, + download_assets: bool = settings.DOWNLOAD_ASSETS, ) -> Sequence[tuple[str, str]]: """ >>> from doc.domain import resource_lookup @@ -376,9 +383,10 @@ def resource_types( for url, path, resource_type_ in repo_clone_list if "rg" != resource_type_ ] - # Perform batch cloning only on filtered list - batch_clone_git_repos(repos_to_clone) - # Process cloned repositories + if download_assets: + batch_download_repos(repos_to_clone) + else: + batch_clone_git_repos(repos_to_clone) for url, resource_filepath, resource_type in repo_clone_list: if resource_type: book_assets = [] @@ -432,25 +440,66 @@ def resource_types( return sorted(unique_values, key=lambda value: value[1]) +def batch_download_repos( + repos: list[tuple[HttpUrl, str]], + asset_caching_enabled: bool = settings.ASSET_CACHING_ENABLED, + asset_caching_period: int = settings.ASSET_CACHING_PERIOD, + base_url: HttpUrl = HttpUrl("https://content.bibletranslationtools.org/"), + base_url_replacement: HttpUrl = HttpUrl( + "https://content.bibletranslationtools.org/api/v1/repos/" + ), + resource_assets_dir: str = settings.RESOURCE_ASSETS_DIR, + user_agent_str: str = "wa-doc", +) -> None: + """Batch download repos and then batch unzip repos.""" + download_commands = [] + zip_file_paths = [] + for url, resource_filepath in repos: + if isdir(resource_filepath): + if dir_needs_update(resource_filepath): + logger.info( + f"Removing stale, incomplete, or corrupt repository: {resource_filepath}" + ) + delete_tree(resource_filepath) + else: + logger.info(f"Skipping download: {resource_filepath} already exists.") + continue + zip_url_base = re.sub(str(base_url), str(base_url_replacement), str(url)) + zip_url = f"{zip_url_base}/archive/master.zip" + zip_file_path = f"{resource_filepath}.zip" + download_commands.append( + f"curl -A {user_agent_str} -X 'GET' {zip_url} -H 'accept: application/json' --output {zip_file_path} --parallel" + ) + zip_file_paths.append(zip_file_path) + download_command = " && ".join(download_commands) + logger.info(f"Downloading repos with command: {download_command}") + try: + subprocess.check_call(download_command, shell=True) + unzip_commands = [ + f"unzip -q -o {zip_file_path} -d {resource_assets_dir}" + for zip_file_path in zip_file_paths + ] + unzip_command = " && ".join(unzip_commands) + logger.info(f"Unzipping downloaded files with command: {unzip_command}") + subprocess.check_call(unzip_command, shell=True) + except subprocess.CalledProcessError: + logger.error("Batch download or unzip failed!") + def batch_clone_git_repos( repos: list[tuple[HttpUrl, str]], asset_caching_enabled: bool = settings.ASSET_CACHING_ENABLED, asset_caching_period: int = settings.ASSET_CACHING_PERIOD, - en_rg: str = settings.EN_RG_DIR, + user_agent_str: str = "wa-doc", ) -> None: """ Clones multiple git repositories in a single batch operation. - If a repository already exists and is fully cloned, it is skipped. - If a repository exists but is a partial clone (corrupt or missing key files), it is removed first. - - The 'en_rg' directory is preserved and never deleted or cloned. - - If asset_caching_enabled is False, repositories are always deleted and re-cloned (except 'en_rg'). + - If asset_caching_enabled is False, repositories are always deleted and re-cloned. """ clone_commands = [] for url, resource_filepath in repos: if isdir(resource_filepath): - if basename(resource_filepath) == en_rg: - logger.info(f"Preserving special directory: {resource_filepath}") - continue git_dir = join(resource_filepath, ".git") if asset_caching_enabled: if isdir(git_dir): @@ -480,7 +529,7 @@ def batch_clone_git_repos( f"Asset caching disabled: forcibly removing {resource_filepath}" ) shutil.rmtree(resource_filepath) - clone_command = f"git clone --depth=1 '{url}' '{resource_filepath}' || true" + clone_command = f"git -c http.userAgent={user_agent_str} clone --depth=1 --single-branch '{url}' '{resource_filepath}' || true" clone_commands.append(clone_command) if clone_commands: full_command = " && ".join(clone_commands) @@ -788,6 +837,7 @@ def get_book_codes_for_lang( use_localized_book_name: bool, usfm_only: bool = False, check_usfm: bool = False, + download_assets: bool = settings.DOWNLOAD_ASSETS, ) -> Sequence[tuple[str, str]]: data = fetch_source_data() if data is None: @@ -815,7 +865,10 @@ def get_book_codes_for_lang( repos_to_clone = [ (url, path) for url, path in repo_clone_list if "en_rg" not in path ] - batch_clone_git_repos(repos_to_clone) + if download_assets: + batch_download_repos(repos_to_clone) + else: + batch_clone_git_repos(repos_to_clone) for url, resource_filepath in repo_clone_list: for repo_info in augmented_repos_info: if repo_info.repo_url == url: diff --git a/backend/doc/utils/file_utils.py b/backend/doc/utils/file_utils.py index e3625774..208ab9f8 100644 --- a/backend/doc/utils/file_utils.py +++ b/backend/doc/utils/file_utils.py @@ -131,6 +131,24 @@ def file_needs_update( return True +def dir_needs_update( + dir_path: str | Path, + asset_caching_enabled: bool = settings.ASSET_CACHING_ENABLED, + asset_caching_period: int = settings.ASSET_CACHING_PERIOD, +) -> bool: + if not asset_caching_enabled: + return True + try: + path = Path(dir_path) + stat = path.stat() + mod_time = datetime.fromtimestamp(stat.st_mtime) + expiry = timedelta(minutes=asset_caching_period) + return datetime.now() - mod_time > expiry + except FileNotFoundError: + logger.debug("Cache miss for %s", dir_path) + return True + + def html_filepath( document_request_key: str, output_dir: str = settings.DOCUMENT_OUTPUT_DIR ) -> str: From 3c9d3ba6a14bf942a56442d948009ee41c5ebd16 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 27 Jun 2025 14:13:00 -0700 Subject: [PATCH 082/208] Simplify function and make title cover all resources This fixes a bug whereby if USFM was chosen only USFM resource names would show in the first page document title. Now it includes a description of all the resources chosen in the document title page. Logic is simplified too. --- backend/doc/domain/document_generator.py | 70 ++++++------------------ 1 file changed, 16 insertions(+), 54 deletions(-) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index cc420faa..8735a67c 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -806,61 +806,23 @@ def check_content_for_issues( def get_languages_title_page_strings( resource_lookup_dtos: Sequence[ResourceLookupDto], usfm_books: Sequence[USFMBook], + book_names: dict[str, str] = BOOK_NAMES, ) -> tuple[str, str]: - lang_codes = list( - {resource_lookup_dto.lang_code for resource_lookup_dto in resource_lookup_dtos} - ) - lang0_book_names = set() - lang0_resource_type_names = set() - lang1_book_names = set() - lang1_resource_type_names = set() - lang0_title, lang1_title = "", "" - if usfm_books: - lang0_books = [ - usfm_book - for usfm_book in usfm_books - if usfm_book.lang_code == lang_codes[0] - ] - for book in lang0_books: - if book.national_book_name: - lang0_book_names.add(book.national_book_name) - lang0_resource_type_names.add(book.resource_type_name) - if lang0_books: - lang0_title = f"{lang0_books[0].lang_name} ({lang0_books[0].localized_lang_name}): {', '.join(sorted(lang0_resource_type_names))} for {', '.join(sorted(lang0_book_names))}" - else: - language0_resource_lookup_dtos = [ - resource_lookup_dto - for resource_lookup_dto in resource_lookup_dtos - if resource_lookup_dto.lang_code == lang_codes[0] - ] - for dto in language0_resource_lookup_dtos: - lang0_book_names.add(BOOK_NAMES[dto.book_code]) - lang0_resource_type_names.add(dto.resource_type_name) - if language0_resource_lookup_dtos: - lang0_title = f"{language0_resource_lookup_dtos[0].lang_name} ({language0_resource_lookup_dtos[0].localized_lang_name}): {', '.join(sorted(lang0_resource_type_names))} for {', '.join(sorted(lang0_book_names))}" - if len(lang_codes) > 1: - lang1_books = [ - usfm_book - for usfm_book in usfm_books - if usfm_book.lang_code == lang_codes[1] - ] - for book in lang1_books: - if book.national_book_name: - lang1_book_names.add(book.national_book_name) - lang1_resource_type_names.add(book.resource_type_name) - if lang1_books: - lang1_title = f"{lang1_books[0].lang_name} ({lang1_books[0].localized_lang_name}): {', '.join(sorted(lang1_resource_type_names))} for {', '.join(sorted(lang1_book_names))}" - else: - language1_resource_lookup_dtos = [ - resource_lookup_dto - for resource_lookup_dto in resource_lookup_dtos - if resource_lookup_dto.lang_code == lang_codes[1] - ] - for dto in language1_resource_lookup_dtos: - lang1_book_names.add(BOOK_NAMES[dto.book_code]) - lang1_resource_type_names.add(dto.resource_type_name) - if language1_resource_lookup_dtos: - lang1_title = f"{language1_resource_lookup_dtos[0].lang_name} ({language1_resource_lookup_dtos[0].localized_lang_name}): {', '.join(sorted(lang1_resource_type_names))} for {', '.join(sorted(lang1_book_names))}" + lang_codes = list({dto.lang_code for dto in resource_lookup_dtos}) + + def get_language_details(lang_code: str) -> str: + book_names_set = set() + resource_type_names_set = set() + dtos = [dto for dto in resource_lookup_dtos if dto.lang_code == lang_code] + for dto in dtos: + book_names_set.add(book_names[dto.book_code]) + resource_type_names_set.add(dto.resource_type_name) + if dtos: + return f"{dtos[0].lang_name} ({dtos[0].localized_lang_name}): {', '.join(sorted(resource_type_names_set))} for {', '.join(sorted(book_names_set))}" + return "" + + lang0_title = get_language_details(lang_codes[0]) + lang1_title = get_language_details(lang_codes[1]) if len(lang_codes) > 1 else "" return lang0_title, lang1_title From a7f214ab4c55955ee727fa30f4600038fafba39b Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 27 Jun 2025 14:15:12 -0700 Subject: [PATCH 083/208] Remove unused package --- backend/requirements.in | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/requirements.in b/backend/requirements.in index 8f667bba..6c41b4ec 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -11,7 +11,6 @@ docxtpl fastapi[all] flower htmldocx -html2docx gunicorn jinja2 mistune From 210eb1d46996314fa0acbdb3d118c5e21f4632c0 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 27 Jun 2025 14:15:41 -0700 Subject: [PATCH 084/208] Update Makefile build rules for usfmparserapi docker service Updates were made to github publish which requires this change to build locally. --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index d89356df..0f059a66 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ install-cython: checkvenv .PHONY: build build: checkvenv down clean-mypyc-artifacts install-cython local-install-deps-dev local-install-deps-prod export IMAGE_TAG=local && \ + docker build --progress=plain -t wycliffeassociates/doc-usfmparser:$${IMAGE_TAG} ./dotnet/USFMParserApi && \ docker build --progress=plain -t wycliffeassociates/doc:$${IMAGE_TAG} . && \ docker build --progress=plain -t wycliffeassociates/doc-ui:$${IMAGE_TAG} ./frontend && \ docker build --progress=plain -t wycliffeassociates/doc-ui-tests:$${IMAGE_TAG} -f ./frontend/testsDockerfile ./frontend @@ -39,6 +40,7 @@ build: checkvenv down clean-mypyc-artifacts install-cython local-install-deps-de .PHONY: build-no-pip-update build-no-pip-update: checkvenv down clean-mypyc-artifacts export IMAGE_TAG=local && \ + docker build --progress=plain -t wycliffeassociates/doc-usfmparser:$${IMAGE_TAG} ./dotnet/USFMParserApi && \ docker build --progress=plain -t wycliffeassociates/doc:$${IMAGE_TAG} . && \ docker build --progress=plain -t wycliffeassociates/doc-ui:$${IMAGE_TAG} ./frontend && \ docker build --progress=plain -t wycliffeassociates/doc-ui-tests:$${IMAGE_TAG} -f ./frontend/testsDockerfile ./frontend @@ -46,6 +48,7 @@ build-no-pip-update: checkvenv down clean-mypyc-artifacts .PHONY: build-no-pip-update-run-tests build-no-pip-update-run-tests: checkvenv down clean-mypyc-artifacts export IMAGE_TAG=local && \ + docker build --progress=plain -t wycliffeassociates/doc-usfmparser:$${IMAGE_TAG} ./dotnet/USFMParserApi && \ docker build --build-arg RUN_TESTS=true --progress=plain -t wycliffeassociates/doc:$${IMAGE_TAG} . && \ docker build --progress=plain -t wycliffeassociates/doc-ui:$${IMAGE_TAG} ./frontend && \ docker build --progress=plain -t wycliffeassociates/doc-ui-tests:$${IMAGE_TAG} -f ./frontend/testsDockerfile ./frontend @@ -53,6 +56,7 @@ build-no-pip-update-run-tests: checkvenv down clean-mypyc-artifacts .PHONY: build-no-cache build-no-cache: checkvenv down clean-mypyc-artifacts local-install-deps-prod export IMAGE_TAG=local && \ + docker build --progress=plain --no-cache --pull -t wycliffeassociates/doc-usfmparser:$${IMAGE_TAG} ./dotnet/USFMParserApi && \ docker build --progress=plain --no-cache --pull -t wycliffeassociates/doc:$${IMAGE_TAG} . && \ docker build --progress=plain --no-cache --pull -t wycliffeassociates/doc-ui:$${IMAGE_TAG} ./frontend && \ docker build --progress=plain --no-cache --pull -t wycliffeassociates/doc-ui-tests:$${IMAGE_TAG} -f ./frontend/testsDockerfile ./frontend @@ -60,6 +64,7 @@ build-no-cache: checkvenv down clean-mypyc-artifacts local-install-deps-prod .PHONY: build-no-cache-no-pip-update build-no-cache-no-pip-update: checkvenv down clean-mypyc-artifacts export IMAGE_TAG=local && \ + docker build --progress=plain --no-cache --pull -t wycliffeassociates/doc-usfmparser:$${IMAGE_TAG} ./dotnet/USFMParserApi && \ docker build --progress=plain --no-cache --pull -t wycliffeassociates/doc:$${IMAGE_TAG} . && \ docker build --progress=plain --no-cache --pull -t wycliffeassociates/doc-ui:$${IMAGE_TAG} ./frontend && \ docker build --progress=plain --no-cache --pull -t wycliffeassociates/doc-ui-tests:$${IMAGE_TAG} -f ./frontend/testsDockerfile ./frontend From a8b4bee54c4ebfa3e77b8dd59d23f4425b6ba247 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 30 Jun 2025 15:24:23 -0700 Subject: [PATCH 085/208] Consistent use of regular functions over arrow functions --- frontend/src/routes/languages/+page.svelte | 4 ++-- frontend/src/routes/passages/language/+page.svelte | 4 ++-- .../passages/passages/BibleReferenceSelector.svelte | 12 ++++++------ .../src/routes/stet/source_languages/+page.svelte | 4 ++-- .../stet/target_languages/[lang0_code]/+page.svelte | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/src/routes/languages/+page.svelte b/frontend/src/routes/languages/+page.svelte index 35429904..bb4571ae 100644 --- a/frontend/src/routes/languages/+page.svelte +++ b/frontend/src/routes/languages/+page.svelte @@ -29,12 +29,12 @@ // Track if the user manually changed the tab: let userInteracted = false - const selectGatewayTab = () => { + function selectGatewayTab() { userInteracted = true showGatewayLanguages = true } - const selectHeartTab = () => { + function selectHeartTab() { userInteracted = true showGatewayLanguages = false } diff --git a/frontend/src/routes/passages/language/+page.svelte b/frontend/src/routes/passages/language/+page.svelte index f6c5df2d..4535c9b0 100644 --- a/frontend/src/routes/passages/language/+page.svelte +++ b/frontend/src/routes/passages/language/+page.svelte @@ -15,12 +15,12 @@ // Track if the user manually changed the tab: let userInteracted = false - const selectGatewayTab = () => { + function selectGatewayTab() { userInteracted = true showGatewayLanguages = true } - const selectHeartTab = () => { + function selectHeartTab() { userInteracted = true showGatewayLanguages = false } diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index f7571537..45793738 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -124,7 +124,7 @@ } } - const handleBookChange = (event: Event) => { + function handleBookChange(event: Event) { const target = event.target as HTMLSelectElement selectedBookCode = target.value console.log('Book Selected:', selectedBookCode) @@ -134,13 +134,13 @@ console.log('Chapters for selected book:', chaptersForSelectedBook) } - const handleChapterChange = (event: Event) => { + function handleChapterChange(event: Event) { const target = event.target as HTMLSelectElement selectedChapter = target.value console.log('Chapter selected:', selectedChapter) } - const handleVerseInput = (event: Event) => { + function handleVerseInput(event: Event) { const target = event.target as HTMLInputElement verseReference = target.value } @@ -165,7 +165,7 @@ } } - const handleNTSurveyCheckboxClick = (event: Event) => { + function handleNTSurveyCheckboxClick(event: Event) { const target = event.target as HTMLInputElement if (target.checked) { handleAddNTSurveyRGPassagesClick() @@ -189,14 +189,14 @@ } } - const handleSTETCheckboxClick = (event: Event) => { + function handleSTETCheckboxClick(event: Event) { const target = event.target as HTMLInputElement if (target.checked) { handleAddSTETPassagesClick() } } - const addPassage = () => { + function addPassage() { if (selectedBookCode && selectedChapter && verseReference) { const bookName = bookCodesAndNames.find(([code]) => code === selectedBookCode)?.[1] ?? 'Unknown' diff --git a/frontend/src/routes/stet/source_languages/+page.svelte b/frontend/src/routes/stet/source_languages/+page.svelte index 6e3489ed..48e92893 100644 --- a/frontend/src/routes/stet/source_languages/+page.svelte +++ b/frontend/src/routes/stet/source_languages/+page.svelte @@ -27,12 +27,12 @@ // Track if the user manually changed the tab: let userInteracted = false - const selectGatewayTab = () => { + function selectGatewayTab() { userInteracted = true showGatewayLanguages = true } - const selectHeartTab = () => { + function selectHeartTab() { userInteracted = true showGatewayLanguages = false } diff --git a/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte b/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte index 8e43e70b..df9a8050 100644 --- a/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte +++ b/frontend/src/routes/stet/target_languages/[lang0_code]/+page.svelte @@ -40,12 +40,12 @@ // Track if the user manually changed the tab let userInteracted = false - const selectGatewayTab = () => { + function selectGatewayTab() { userInteracted = true showGatewayLanguages = true } - const selectHeartTab = () => { + function selectHeartTab() { userInteracted = true showGatewayLanguages = false } From 07fa33f303d70199f52a0f2852f68af9e9eb3d71 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 30 Jun 2025 15:27:07 -0700 Subject: [PATCH 086/208] Add ability to remove NT Survey passages or STET passages en masse By request of product owner --- .../src/lib/passages/stores/PassagesStore.ts | 23 ++++ .../passages/BibleReferenceSelector.svelte | 110 ++++++++++++++++-- 2 files changed, 122 insertions(+), 11 deletions(-) diff --git a/frontend/src/lib/passages/stores/PassagesStore.ts b/frontend/src/lib/passages/stores/PassagesStore.ts index c785513b..ecf38357 100644 --- a/frontend/src/lib/passages/stores/PassagesStore.ts +++ b/frontend/src/lib/passages/stores/PassagesStore.ts @@ -42,3 +42,26 @@ export const addPassageReference = ( ] }) } + +export const removePassageReference = ( + langCode: string, + bookCode: string, + startChapterNum: number, + startChapterVerseReference: string, + endChapterNum?: number | null, + endChapterVerseReference?: string | null +) => { + passagesStore.update((currentPassages) => { + return currentPassages.filter( + (p) => + !( + p.langCode === langCode && + p.bookCode === bookCode && + p.startChapterNum === startChapterNum && + p.startChapterVerseReference === startChapterVerseReference && + p.endChapterNum === endChapterNum && + p.endChapterVerseReference === endChapterVerseReference + ) + ) + }) +} diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index 45793738..da513410 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -1,5 +1,9 @@
@@ -352,12 +361,12 @@
@@ -444,6 +453,15 @@ transform: rotate(360deg); } } +*:global(.add-passage-button-disabled) { + background: + linear-gradient(180deg, #a3c1ff 0%, #8bb3ff 100%), /* lighter blue */ + linear-gradient(0deg, #33447e, #33447e); +} +* :global(.add-passage-button-disabled:hover) { + background: + linear-gradient(180deg, #cce4ff 0%, #a3c1ff 100%), linear-gradient(0deg, #33447e, #33447e); +} * :global(.add-passage-button) { background: linear-gradient(180deg, #1876fd 0%, #015ad9 100%), linear-gradient(0deg, #33445c, #33445c); From dbee3bf0dcde9c8e5e896e3cc5b0382899837b7e Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 1 Jul 2025 18:59:51 -0700 Subject: [PATCH 089/208] Get caching of downloaded zipped repos working --- backend/doc/domain/resource_lookup.py | 13 ++++++++----- backend/doc/utils/file_utils.py | 18 ------------------ 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index a6a56efc..ff3bad52 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -10,7 +10,7 @@ import shutil import subprocess from glob import glob -from os import scandir, stat +from os import remove, scandir, stat from os.path import exists, isdir, join from pathlib import Path from typing import Mapping, Optional, Sequence @@ -41,7 +41,6 @@ ) from doc.utils.file_utils import ( delete_tree, - dir_needs_update, file_needs_update, make_dir, read_file, @@ -459,18 +458,22 @@ def batch_download_repos( download_commands = [] zip_file_paths = [] for url, resource_filepath in repos: + zip_file_path = f"{resource_filepath}.zip" if isdir(resource_filepath): - if dir_needs_update(resource_filepath): + # Instead of checking the directory which was created from + # a zip with timestamps current when the zip file was created (which + # would likely be quite old), we instead check the zip file itself. + if file_needs_update(zip_file_path): logger.info( - f"Removing stale, incomplete, or corrupt repository: {resource_filepath}" + f"Removing stale, incomplete, or corrupt repository: {resource_filepath} and zip file: {zip_file_path}" ) + remove(zip_file_path) delete_tree(resource_filepath) else: logger.info(f"Skipping download: {resource_filepath} already exists.") continue zip_url_base = re.sub(str(base_url), str(base_url_replacement), str(url)) zip_url = f"{zip_url_base}/archive/master.zip" - zip_file_path = f"{resource_filepath}.zip" download_commands.append( f"curl -A {user_agent_str} -X 'GET' {zip_url} -H 'accept: application/json' --output {zip_file_path} --parallel" ) diff --git a/backend/doc/utils/file_utils.py b/backend/doc/utils/file_utils.py index 208ab9f8..e3625774 100644 --- a/backend/doc/utils/file_utils.py +++ b/backend/doc/utils/file_utils.py @@ -131,24 +131,6 @@ def file_needs_update( return True -def dir_needs_update( - dir_path: str | Path, - asset_caching_enabled: bool = settings.ASSET_CACHING_ENABLED, - asset_caching_period: int = settings.ASSET_CACHING_PERIOD, -) -> bool: - if not asset_caching_enabled: - return True - try: - path = Path(dir_path) - stat = path.stat() - mod_time = datetime.fromtimestamp(stat.st_mtime) - expiry = timedelta(minutes=asset_caching_period) - return datetime.now() - mod_time > expiry - except FileNotFoundError: - logger.debug("Cache miss for %s", dir_path) - return True - - def html_filepath( document_request_key: str, output_dir: str = settings.DOCUMENT_OUTPUT_DIR ) -> str: From b0bb01d20108266b8f83c79db64fc6ff40d80bbb Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 1 Jul 2025 19:02:46 -0700 Subject: [PATCH 090/208] Send additional headers to try to bypass cloudflare limiting More to be done, as it was recommended by dev-ops, but does not seem to work as advertised. More to come. --- backend/doc/domain/resource_lookup.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index ff3bad52..f7c43a4c 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -197,12 +197,15 @@ # present. It makes it seem like a bug in STET and is bad UX. LANG_CODES_WITH_NO_USFM: list[str] = ["ru"] +# For cloudflare USER_AGENT_STR: str = "wa-doc" +X_REQUESTED_WITH_VALUE: str = "WA-Tool-Doc" def fetch_source_data( data_api_url: HttpUrl = settings.DATA_API_URL, user_agent_str: str = USER_AGENT_STR, + x_requested_with_value: str = X_REQUESTED_WITH_VALUE, ) -> Optional[SourceData]: """ Downloads data from a GraphQL API. @@ -232,7 +235,7 @@ def fetch_source_data( } """ query_json = {"query": graphql_query} - headers = {"User-Agent": user_agent_str} + headers = {"User-Agent": user_agent_str, "X-Requested-With": x_requested_with_value} try: response = requests.post(str(data_api_url), json=query_json, headers=headers) if response.status_code == 200: @@ -453,6 +456,7 @@ def batch_download_repos( ), resource_assets_dir: str = settings.RESOURCE_ASSETS_DIR, user_agent_str: str = USER_AGENT_STR, + x_requested_with_value: str = X_REQUESTED_WITH_VALUE, ) -> None: """Batch download repos and then batch unzip repos.""" download_commands = [] @@ -475,7 +479,7 @@ def batch_download_repos( zip_url_base = re.sub(str(base_url), str(base_url_replacement), str(url)) zip_url = f"{zip_url_base}/archive/master.zip" download_commands.append( - f"curl -A {user_agent_str} -X 'GET' {zip_url} -H 'accept: application/json' --output {zip_file_path} --parallel" + f"curl -A {user_agent_str} -H 'X-Requested-With: {x_requested_with_value}' -X 'GET' {zip_url} -H 'accept: application/json' --output {zip_file_path} --parallel" ) zip_file_paths.append(zip_file_path) download_command = " && ".join(download_commands) @@ -502,6 +506,7 @@ def batch_clone_git_repos( asset_caching_enabled: bool = settings.ASSET_CACHING_ENABLED, asset_caching_period: int = settings.ASSET_CACHING_PERIOD, user_agent_str: str = USER_AGENT_STR, + x_requested_with_value: str = X_REQUESTED_WITH_VALUE, ) -> None: """ Clones multiple git repositories in a single batch operation. @@ -541,7 +546,7 @@ def batch_clone_git_repos( f"Asset caching disabled: forcibly removing {resource_filepath}" ) shutil.rmtree(resource_filepath) - clone_command = f"git -c http.userAgent={user_agent_str} clone --depth=1 --single-branch '{url}' '{resource_filepath}' || true" + clone_command = f"git -c http.userAgent={user_agent_str} http.extraHeader=X-Requested-With: {x_requested_with_value} clone --depth=1 --single-branch '{url}' '{resource_filepath}' || true" clone_commands.append(clone_command) if clone_commands: full_command = " && ".join(clone_commands) From 598484fef89ce3d6abc5adaab346148befb522bc Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 2 Jul 2025 13:22:07 -0700 Subject: [PATCH 091/208] Check if zip file exists before deleting To avoid potential exceptions due to missing file --- backend/doc/domain/resource_lookup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index f7c43a4c..5933286f 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -10,7 +10,7 @@ import shutil import subprocess from glob import glob -from os import remove, scandir, stat +from os import scandir, stat from os.path import exists, isdir, join from pathlib import Path from typing import Mapping, Optional, Sequence @@ -462,7 +462,7 @@ def batch_download_repos( download_commands = [] zip_file_paths = [] for url, resource_filepath in repos: - zip_file_path = f"{resource_filepath}.zip" + zip_file_path = Path(f"{resource_filepath}.zip") if isdir(resource_filepath): # Instead of checking the directory which was created from # a zip with timestamps current when the zip file was created (which @@ -471,7 +471,8 @@ def batch_download_repos( logger.info( f"Removing stale, incomplete, or corrupt repository: {resource_filepath} and zip file: {zip_file_path}" ) - remove(zip_file_path) + if zip_file_path.is_file(): + zip_file_path.unlink() delete_tree(resource_filepath) else: logger.info(f"Skipping download: {resource_filepath} already exists.") From ffae9408e186e53347c004e71fe44855224a0fe6 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 2 Jul 2025 20:57:51 -0700 Subject: [PATCH 092/208] Disable a frontend test --- frontend/tests/e2e/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/tests/e2e/test.ts b/frontend/tests/e2e/test.ts index ed2b3d5c..d5addfa1 100644 --- a/frontend/tests/e2e/test.ts +++ b/frontend/tests/e2e/test.ts @@ -159,7 +159,7 @@ test('test optional settings', async ({ page }) => { }) -test('test aba philemon', async ({ page }) => { +test.skip('test aba philemon', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByRole('button', { name: 'Heart' }).click() await page.getByText('Abé aba').click() From 8eefd323603a4baa4cd6fde9fd9d6b4b2c64508e Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 7 Jul 2025 09:53:48 -0700 Subject: [PATCH 093/208] Improved git clone command with headers --- backend/doc/domain/resource_lookup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 5933286f..7aa65a34 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -547,7 +547,11 @@ def batch_clone_git_repos( f"Asset caching disabled: forcibly removing {resource_filepath}" ) shutil.rmtree(resource_filepath) - clone_command = f"git -c http.userAgent={user_agent_str} http.extraHeader=X-Requested-With: {x_requested_with_value} clone --depth=1 --single-branch '{url}' '{resource_filepath}' || true" + clone_command = ( + f"git -c http.userAgent='{user_agent_str}' " + f"-c http.extraHeader='X-Requested-With:{x_requested_with_value}' " + f"clone --depth=1 --single-branch '{url}' '{resource_filepath}' || true" + ) clone_commands.append(clone_command) if clone_commands: full_command = " && ".join(clone_commands) From d3d1dc51483b3e8ea536d1570da0989625cb6382 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 7 Jul 2025 10:53:13 -0700 Subject: [PATCH 094/208] Update requirements.txt via pip tools Remove html2docx which we don't use --- backend/requirements.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 10ba4a19..be62231a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -78,8 +78,6 @@ h11==0.14.0 # via # httpcore # uvicorn -html2docx==1.6.0 - # via -r ./backend/requirements.in htmldocx==0.0.6 # via -r ./backend/requirements.in httpcore==1.0.7 @@ -156,7 +154,6 @@ python-docx==1.1.0 # via # docxcompose # docxtpl - # html2docx # htmldocx python-dotenv==1.0.1 # via @@ -198,7 +195,6 @@ termcolor==2.5.0 tinycss2==1.4.0 # via # cssselect2 - # html2docx # weasyprint tinyhtml5==2.0.0 # via weasyprint From 21c71d83521d6c991b7ee3e78b39047466f9dd0a Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 8 Jul 2025 09:33:06 -0700 Subject: [PATCH 095/208] Switch back to git for asset acquisition Some assets are not found in zip form correctly at this time. --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index df33168f..e73693c0 100644 --- a/.env +++ b/.env @@ -68,7 +68,7 @@ CHECK_ALL_BOOKS_FOR_LANGUAGE=true # When true the system will acquire git repos via download of its # master.zip file. When false it will clone the repo. Both approaches # are optimized maximally by taking advantage of curl and git options. -DOWNLOAD_ASSETS=true +DOWNLOAD_ASSETS=false # * http://localhost:3000 covers requests originating from the case # where 'npm run dev' is invoked to run vite (to run svelte js frontend) From 39e0d13287eb3acb72a1af898415a4cb2240a279 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 9 Jul 2025 13:01:47 -0700 Subject: [PATCH 096/208] New STET input doc for Tok Pisin (tpi) language --- backend/stet/data/stet_tpi.docx | Bin 0 -> 107823 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/stet/data/stet_tpi.docx diff --git a/backend/stet/data/stet_tpi.docx b/backend/stet/data/stet_tpi.docx new file mode 100644 index 0000000000000000000000000000000000000000..780c47eb4a455b46557f719bca7073d74d8c6c8e GIT binary patch literal 107823 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 literal 0 HcmV?d00001 From dc174971a408f50b2bfeb4f996e5cbd9da481b84 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 9 Jul 2025 13:02:27 -0700 Subject: [PATCH 097/208] Automatic update to later Python version for packages --- backend/requirements-prod.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements-prod.txt b/backend/requirements-prod.txt index 3b9a5831..4c4a8093 100644 --- a/backend/requirements-prod.txt +++ b/backend/requirements-prod.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile ./backend/requirements-prod.in From c3ba72b5a1a212a828aea7873cb32ad818f524df Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 9 Jul 2025 13:03:17 -0700 Subject: [PATCH 098/208] Add STET translation strings for Tok Pisin (tpi) --- backend/stet/domain/strings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/stet/domain/strings.py b/backend/stet/domain/strings.py index 69eaded6..ca78bb39 100644 --- a/backend/stet/domain/strings.py +++ b/backend/stet/domain/strings.py @@ -2,22 +2,26 @@ "en": "Spiritual Terms Evaluation Tool (STET)", "es-419": "Herramienta de Evaluación de Términos Espirituales (STET)", "pt-br": "Ferramenta de Avaliação de Termos Espirituais (STET)", + "tpi": "Tul bilong skelim ol spirit tok bilong buk trenslesen (STET)", } TRANSLATED_FOOTER_PHRASES_TABLE: dict[str, str] = { "en": "Generated on", "es-419": "Generado el", "pt-br": "Gerado em", + "tpi": "Wok i bin kamap long", } LOCALIZED_DATE_FORMAT_STRINGS: dict[str, str] = { "en": "%m/%d/%Y %H:%M:%S", "es-419": "%d/%m/%Y %H:%M:%S", "pt-br": "%d/%m/%Y %H:%M:%S", + "tpi": "%d/%m/%Y %H:%M:%S", } TRANSLATED_TABLE_COLUMN_HEADERS = { "en": ("Source Reference", "Target Reference", "Status", "OK"), "es-419": ("Fuente", "Idioma Materna", "Estado", "OK"), "pt-br": ("Referência de Origem", "Referência de Destino", "Status", "OK"), + "tpi": ("As bilong dispela", "As bilong mak", "Stet", "OK"), } From 631530bd6fd0bb43a2558b5d0ac68aa550321464 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 9 Jul 2025 13:38:58 -0700 Subject: [PATCH 099/208] Add frontend test for STET Tok Pisin (tpi) input language --- frontend/tests/e2e/stet_test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/tests/e2e/stet_test.ts b/frontend/tests/e2e/stet_test.ts index de0135c5..da6bc232 100644 --- a/frontend/tests/e2e/stet_test.ts +++ b/frontend/tests/e2e/stet_test.ts @@ -42,6 +42,14 @@ test.describe('Desktop Tests', () => { await page.getByRole('button', { name: 'Submit' }).click() await page.getByRole('button', { name: 'Generate File' }).click() }) + test('test tok pisin input language', async ({ page }) => { + await page.goto('http://localhost:8001/stet') + await page.getByText('Tok Pisin').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByLabel('English en').check() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Generate File' }).click() + }) }) // Separate group for mobile tests From 9db72f609aed9b95d594c85a8db76539b7ea3e89 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 9 Jul 2025 13:40:20 -0700 Subject: [PATCH 100/208] Update a test name and allow prettier to autoformat --- frontend/tests/e2e/stet_test.ts | 139 ++++++++++++++++---------------- 1 file changed, 70 insertions(+), 69 deletions(-) diff --git a/frontend/tests/e2e/stet_test.ts b/frontend/tests/e2e/stet_test.ts index da6bc232..10d99c47 100644 --- a/frontend/tests/e2e/stet_test.ts +++ b/frontend/tests/e2e/stet_test.ts @@ -2,46 +2,47 @@ import { expect, test } from '@playwright/test' // Group tests with specific settings test.describe('Desktop Tests', () => { - test.use({ - viewport: { - height: 700, - width: 1200, - }, - }) + test.use({ + viewport: { + height: 700, + width: 1200 + } + }) - test('test stet', async ({ page }) => { - await page.goto('http://localhost:8001/stet') - await page.getByLabel('English en').check() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Cebuano').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Generate File' }).click() - }) + test('test stet', async ({ page }) => { + await page.goto('http://localhost:8001/stet') + await page.getByLabel('English en').check() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Cebuano').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Generate File' }).click() + }) + + test('test next back and edit', async ({ page }) => { + await page.goto('http://localhost:8001/stet') + await page.getByText('English').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Back' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Bichelamar (Bislama)').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('link', { name: 'Target Language' }).click() + await page.getByRole('link', { name: 'Source Language' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Back' }).click() + await page.getByRole('button', { name: 'Back' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Edit' }).click() + await page.getByRole('button', { name: 'Edit' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByLabel('Email me a copy of my').check() + await page.getByPlaceholder('Type email address here (').click() + await page.getByPlaceholder('Type email address here (').fill('fake@example.com') + await page.getByRole('button', { name: 'Submit' }).click() + await page.getByRole('button', { name: 'Generate File' }).click() + }) - test('test stet 2', async ({ page }) => { - await page.goto('http://localhost:8001/stet') - await page.getByText('English').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Back' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Bichelamar (Bislama)').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('link', { name: 'Target Language' }).click() - await page.getByRole('link', { name: 'Source Language' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Back' }).click() - await page.getByRole('button', { name: 'Back' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Edit' }).click() - await page.getByRole('button', { name: 'Edit' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByLabel('Email me a copy of my').check() - await page.getByPlaceholder('Type email address here (').click() - await page.getByPlaceholder('Type email address here (').fill('fake@example.com') - await page.getByRole('button', { name: 'Submit' }).click() - await page.getByRole('button', { name: 'Generate File' }).click() - }) test('test tok pisin input language', async ({ page }) => { await page.goto('http://localhost:8001/stet') await page.getByText('Tok Pisin').click() @@ -54,37 +55,37 @@ test.describe('Desktop Tests', () => { // Separate group for mobile tests test.describe('Mobile Tests', () => { - test.use({ - viewport: { - height: 600, - width: 300, - }, - }) + test.use({ + viewport: { + height: 600, + width: 300 + } + }) - test('test mobile', async ({ page }) => { - await page.goto('http://localhost:8001/stet') - await page.getByLabel('English en').check() - await page.getByRole('button').nth(1).click() - await page.getByText('Bahasa Indonesia (Indonesian)').click() - await page.getByRole('button').nth(1).click() - await page.getByRole('button', { name: '2' }).click() - await page.getByRole('button', { name: 'Edit' }).nth(1).click() - await page.getByRole('button').nth(1).click() - await page.getByRole('button', { name: 'Generate File' }).click() - }) + test('test mobile', async ({ page }) => { + await page.goto('http://localhost:8001/stet') + await page.getByLabel('English en').check() + await page.getByRole('button').nth(1).click() + await page.getByText('Bahasa Indonesia (Indonesian)').click() + await page.getByRole('button').nth(1).click() + await page.getByRole('button', { name: '2' }).click() + await page.getByRole('button', { name: 'Edit' }).nth(1).click() + await page.getByRole('button').nth(1).click() + await page.getByRole('button', { name: 'Generate File' }).click() + }) - test('test mobile 2', async ({ page }) => { - await page.goto('http://localhost:8001/stet') - await page.getByText('English').click() - await page.getByRole('button').nth(1).click() - await page.getByRole('button').first().click() - await page.getByRole('button').nth(1).click() - await page.getByRole('button').nth(2).click() - await page.getByText('Heart languages').click() - await page.getByRole('button', { name: 'Close' }).click() - await page.getByText('Abé').click() - await page.getByRole('button').nth(1).click() - await page.getByRole('button').first().click() - await expect(page.getByLabel('Abé aba')).toBeChecked({timeout: 1200000}) - }) + test('test mobile 2', async ({ page }) => { + await page.goto('http://localhost:8001/stet') + await page.getByText('English').click() + await page.getByRole('button').nth(1).click() + await page.getByRole('button').first().click() + await page.getByRole('button').nth(1).click() + await page.getByRole('button').nth(2).click() + await page.getByText('Heart languages').click() + await page.getByRole('button', { name: 'Close' }).click() + await page.getByText('Abé').click() + await page.getByRole('button').nth(1).click() + await page.getByRole('button').first().click() + await expect(page.getByLabel('Abé aba')).toBeChecked({ timeout: 1200000 }) + }) }) From 2a778018b488e6a6231e711bf342e52eaeb18711 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 10 Jul 2025 09:20:36 -0700 Subject: [PATCH 101/208] Update a few translated phrases for STET tpi By request of PO --- backend/stet/data/stet_tpi.docx | Bin 107823 -> 84166 bytes backend/stet/domain/strings.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/stet/data/stet_tpi.docx b/backend/stet/data/stet_tpi.docx index 780c47eb4a455b46557f719bca7073d74d8c6c8e..596aac99d749219f58584dab8f4a04eca3a0ebf4 100644 GIT binary patch delta 65762 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 A&;S4c diff --git a/backend/stet/domain/strings.py b/backend/stet/domain/strings.py index ca78bb39..37988b21 100644 --- a/backend/stet/domain/strings.py +++ b/backend/stet/domain/strings.py @@ -23,5 +23,5 @@ "en": ("Source Reference", "Target Reference", "Status", "OK"), "es-419": ("Fuente", "Idioma Materna", "Estado", "OK"), "pt-br": ("Referência de Origem", "Referência de Destino", "Status", "OK"), - "tpi": ("As bilong dispela", "As bilong mak", "Stet", "OK"), + "tpi": ("Narapela baibel ves", "Tokples ves", "Sek", "OK"), } From de48a415f17b360edfde6a7daca08c9652678c90 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 10 Jul 2025 09:58:50 -0700 Subject: [PATCH 102/208] Accumulate then interpret parts of Document into whole Centralize imperative logic rather than having it strewn about. --- ...ly_strategies_book_then_lang_by_chapter.py | 270 +++++++-------- ...ly_strategies_lang_then_book_by_chapter.py | 312 ++++++++---------- .../assembly_strategy_utils.py | 27 +- backend/doc/domain/document_generator.py | 78 +++-- backend/doc/domain/model.py | 8 + 5 files changed, 327 insertions(+), 368 deletions(-) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py index d5b0d026..b8a977c9 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py @@ -10,17 +10,12 @@ tn_chapter_verses, tq_chapter_verses, ) -from doc.domain.assembly_strategies_docx.assembly_strategy_utils import ( - add_one_column_section, - add_page_break, - add_two_column_section, - create_docx_subdoc, -) from doc.domain.bible_books import BOOK_CHAPTERS, BOOK_NAMES from doc.domain.model import ( AssemblyLayoutEnum, BCBook, ChunkSizeEnum, + DocumentPart, LangDirEnum, TNBook, TQBook, @@ -28,8 +23,6 @@ USFMBook, ) from doc.reviewers_guide.model import RGBook -from docx import Document # type: ignore -from docxcompose.composer import Composer # type: ignore logger = settings.logger(__name__) @@ -46,12 +39,13 @@ def assemble_content_by_book_then_lang( assembly_layout_kind: AssemblyLayoutEnum, chunk_size: ChunkSizeEnum, book_names: Mapping[str, str] = BOOK_NAMES, -) -> Composer: +) -> list[DocumentPart]: """ Assemble by book then by language in alphabetic order before delegating more atomic ordering/interleaving to an assembly sub-strategy. """ + document_parts: list[DocumentPart] = [] # Sort the books in canonical order so that groupby does what we want. book_id_map = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) most_book_codes = max( @@ -88,7 +82,7 @@ def assemble_content_by_book_then_lang( rg_book for rg_book in rg_books if rg_book.book_code == book_code ] if selected_usfm_books: - composer = assemble_usfm_by_chapter( + document_parts = assemble_usfm_by_chapter( usfm_books, tn_books, tq_books, @@ -96,9 +90,9 @@ def assemble_content_by_book_then_lang( bc_books, rg_books, ) - return composer + return document_parts elif not selected_usfm_books and selected_tn_books: - composer = assemble_tn_by_chapter( + document_parts = assemble_tn_by_chapter( usfm_books, tn_books, tq_books, @@ -106,9 +100,9 @@ def assemble_content_by_book_then_lang( bc_books, rg_books, ) - return composer + return document_parts elif not selected_usfm_books and not selected_tn_books and selected_tq_books: - composer = assemble_tq_by_chapter( + document_parts = assemble_tq_by_chapter( usfm_books, tn_books, tq_books, @@ -116,14 +110,14 @@ def assemble_content_by_book_then_lang( bc_books, rg_books, ) - return composer + return document_parts elif ( not selected_usfm_books and not selected_tn_books and not selected_tq_books and (selected_tw_books or selected_bc_books or selected_rg_books) ): - composer = assemble_tw_by_chapter( + document_parts = assemble_tw_by_chapter( usfm_books, tn_books, tq_books, @@ -131,7 +125,8 @@ def assemble_content_by_book_then_lang( bc_books, rg_books, ) - return composer + return document_parts + return document_parts def assemble_usfm_by_chapter( @@ -144,7 +139,7 @@ def assemble_usfm_by_chapter( book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, fmt_str: str = BOOK_NAME_FMT_STR, -) -> Composer: +) -> list[DocumentPart]: """ Construct the Docx wherein at least one USFM resource exists, one column layout. @@ -170,73 +165,65 @@ def rg_sort_key(resource: RGBook) -> str: tq_books = sorted(tq_books, key=tq_sort_key) bc_books = sorted(bc_books, key=bc_sort_key) rg_books = sorted(rg_books, key=rg_sort_key) - doc = Document() - composer = Composer(doc) + document_parts: list[DocumentPart] = [] if show_tn_book_intro: for tn_book in tn_books: if tn_book.book_intro: book_intro_ = tn_book.book_intro book_intro_adj = adjust_book_intro_headings(book_intro_) - subdoc = create_docx_subdoc( - book_intro_adj, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=book_intro_adj, + is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) for bc_book in bc_books: # Add the commentary book intro - subdoc = create_docx_subdoc(bc_book.book_intro, bc_book.lang_code) - composer.append(subdoc) + document_parts.append(DocumentPart(content=bc_book.book_intro)) book_codes = {usfm_book.book_code for usfm_book in usfm_books} for book_code in book_codes: num_chapters = book_chapters[book_code] for chapter_num in range(1, num_chapters + 1): - add_one_column_section(doc) # Add chapter intro for each language for tn_book in [ tn_book for tn_book in tn_books if tn_book.book_code == book_code ]: if chapter_num in tn_book.chapters: - subdoc = create_docx_subdoc( - chapter_intro(tn_book, chapter_num), - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=chapter_intro(tn_book, chapter_num), + is_rtl=tn_book + and tn_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) for bc_book in [ bc_book for bc_book in bc_books if bc_book.book_code == book_code ]: if chapter_num in bc_book.chapters: # Add the chapter commentary. - subdoc = create_docx_subdoc( - chapter_commentary(bc_book, chapter_num), - bc_book.lang_code, + document_parts.append( + DocumentPart(content=chapter_commentary(bc_book, chapter_num)) ) - composer.append(subdoc) for usfm_book in [ usfm_book for usfm_book in usfm_books if usfm_book.book_code == book_code ]: - add_one_column_section(doc) - # Add the book title, e.g., 1 Peter - subdoc = create_docx_subdoc( - fmt_str.format(usfm_book.national_book_name), - usfm_book.lang_code, + document_parts.append( + DocumentPart( + content=fmt_str.format(usfm_book.national_book_name), + add_hr_p=False, + ) ) - composer.append(subdoc) if chapter_num in usfm_book.chapters: # Add the interleaved USFM chapters - add_one_column_section(doc) - # fmt: off - is_rtl = usfm_book and usfm_book.lang_direction == LangDirEnum.RTL - # fmt: on - subdoc = create_docx_subdoc( - usfm_book.chapters[chapter_num].content, - usfm_book.lang_code, - is_rtl, + document_parts.append( + DocumentPart( + content=usfm_book.chapters[chapter_num].content, + is_rtl=usfm_book + and usfm_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) # Add the interleaved tn notes tn_verses = None for tn_book in [ @@ -247,13 +234,14 @@ def rg_sort_key(resource: RGBook) -> str: if chapter_num in tn_book.chapters: tn_verses = tn_chapter_verses(tn_book, chapter_num) if tn_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tn_verses, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tn_verses, + is_rtl=tn_book + and tn_book.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) # Add the interleaved tq questions for tq_book in [ tq_book @@ -264,26 +252,27 @@ def rg_sort_key(resource: RGBook) -> str: tq_verses = tq_chapter_verses(tq_book, chapter_num) # Add TQ verse content, if any if tq_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tq_verses, - tq_book.lang_code, - tq_book and tq_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tq_verses, + is_rtl=tq_book + and tq_book.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) for rg_book in [ rg_book for rg_book in rg_books if rg_book.book_code == usfm_book.book_code ]: if chapter_num in rg_book.chapters: - subdoc = create_docx_subdoc( - rg_chapter_verses(rg_book, chapter_num), - rg_book.lang_code, + document_parts.append( + DocumentPart(content=rg_chapter_verses(rg_book, chapter_num)) ) - composer.append(subdoc) - add_page_break(doc) - return composer + document_parts.append( + DocumentPart(content="", add_hr_p=False, add_page_break=True) + ) + return document_parts def assemble_tn_by_chapter( @@ -295,7 +284,7 @@ def assemble_tn_by_chapter( rg_books: Sequence[RGBook], book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, -) -> Composer: +) -> list[DocumentPart]: """ Construct the HTML for a 'by chapter' strategy wherein at least tn_book_content_units exists. @@ -317,32 +306,25 @@ def rg_sort_key(resource: RGBook) -> str: tq_books = sorted(tq_books, key=tq_sort_key) bc_books = sorted(bc_books, key=bc_sort_key) rg_books = sorted(rg_books, key=rg_sort_key) - doc = Document() - composer = Composer(doc) - add_one_column_section(doc) + document_parts: list[DocumentPart] = [] if show_tn_book_intro: # Add book intros for each tn_book for tn_book in tn_books: if tn_book.book_intro: book_intro_ = tn_book.book_intro book_intro_adj = adjust_book_intro_headings(book_intro_) - subdoc = create_docx_subdoc( - book_intro_adj, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=book_intro_adj, + is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) for bc_book in bc_books: - subdoc = create_docx_subdoc( - bc_book_intro(bc_book), - bc_book.lang_code, - ) - composer.append(subdoc) + document_parts.append(DocumentPart(content=bc_book_intro(bc_book))) book_codes = {tn_book.book_code for tn_book in tn_books} for book_code in book_codes: num_chapters = book_chapters[book_code] for chapter_num in range(1, num_chapters + 1): - add_one_column_section(doc) for tn_book in [ tn_book for tn_book in tn_books if tn_book.book_code == book_code ]: @@ -353,22 +335,21 @@ def rg_sort_key(resource: RGBook) -> str: one_column_html.append(chapter_intro(tn_book, chapter_num)) one_column_html_ = "".join(one_column_html) if one_column_html_: - subdoc = create_docx_subdoc( - one_column_html_, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=one_column_html_, + is_rtl=tn_book + and tn_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) for bc_book in [ bc_book for bc_book in bc_books if bc_book.book_code == book_code ]: if chapter_num in bc_book.chapters: # Add the chapter commentary. - subdoc = create_docx_subdoc( - chapter_commentary(bc_book, chapter_num), - bc_book.lang_code, + document_parts.append( + DocumentPart(content=chapter_commentary(bc_book, chapter_num)) ) - composer.append(subdoc) # Add the interleaved tn notes for tn_book in [ tn_book for tn_book in tn_books if tn_book.book_code == book_code @@ -376,13 +357,13 @@ def rg_sort_key(resource: RGBook) -> str: if chapter_num in tn_book.chapters: tn_verses = tn_chapter_verses(tn_book, chapter_num) if tn_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tn_verses, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tn_verses, + is_rtl=tn_book + and tn_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) # Add the interleaved tq questions for tq_book in [ tq_book for tq_book in tq_books if tq_book.book_code == book_code @@ -390,28 +371,30 @@ def rg_sort_key(resource: RGBook) -> str: tq_verses = tq_chapter_verses(tq_book, chapter_num) # Add TQ verse content, if any if tq_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tq_verses, - tq_book.lang_code, - tq_book and tq_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tq_verses, + is_rtl=tq_book + and tq_book.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) for rg_book in [ rg_book for rg_book in rg_books if rg_book.book_code == book_code ]: rg_verses = rg_chapter_verses(rg_book, chapter_num) if rg_verses: - add_one_column_section(doc) - # add_two_column_section(doc) - subdoc = create_docx_subdoc( - rg_verses, - rg_book.lang_code, - rg_book and rg_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=rg_verses, + is_rtl=rg_book + and rg_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) - add_page_break(doc) - return composer + document_parts.append( + DocumentPart(content="", add_hr_p=False, add_page_break=True) + ) + return document_parts def assemble_tq_by_chapter( @@ -422,7 +405,7 @@ def assemble_tq_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], book_chapters: Mapping[str, int] = BOOK_CHAPTERS, -) -> Composer: +) -> list[DocumentPart]: """ Construct the HTML for a 'by chapter' strategy wherein at least tq_book_content_units exists. @@ -440,8 +423,7 @@ def rg_sort_key(resource: RGBook) -> str: tq_books = sorted(tq_books, key=tq_sort_key) bc_books = sorted(bc_books, key=bc_sort_key) rg_books = sorted(rg_books, key=rg_sort_key) - doc = Document() - composer = Composer(doc) + document_parts: list[DocumentPart] = [] book_codes = {tq_book.book_code for tq_book in tq_books} for book_code in book_codes: num_chapters = book_chapters[book_code] @@ -453,9 +435,7 @@ def rg_sort_key(resource: RGBook) -> str: ]: one_column_html.append(chapter_commentary(bc_book, chapter_num)) if one_column_html: - add_one_column_section(doc) - subdoc = create_docx_subdoc("".join(one_column_html), bc_book.lang_code) - composer.append(subdoc) + document_parts.append(DocumentPart(content="".join(one_column_html))) # Add the interleaved tq questions for tq_book in [ tq_book @@ -464,13 +444,14 @@ def rg_sort_key(resource: RGBook) -> str: ]: tq_verses = tq_chapter_verses(tq_book, chapter_num) if tq_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tq_verses, - tq_book.lang_code, - tq_book and tq_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tq_verses, + is_rtl=tq_book + and tq_book.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) for rg_book in [ rg_book for rg_book in rg_books @@ -478,15 +459,17 @@ def rg_sort_key(resource: RGBook) -> str: ]: rg_verses = rg_chapter_verses(rg_book, chapter_num) if rg_verses: - add_one_column_section(doc) - subdoc = create_docx_subdoc( - rg_verses, - rg_book.lang_code, - rg_book and rg_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=rg_verses, + is_rtl=rg_book + and rg_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) - add_page_break(doc) - return composer + document_parts.append( + DocumentPart(content="", add_hr_p=False, add_page_break=True) + ) + return document_parts # This function could be a little confusing for newcomers. TW lives at @@ -503,10 +486,9 @@ def assemble_tw_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], -) -> Composer: +) -> list[DocumentPart]: """Construct the HTML for BC and TW.""" - doc = Document() - composer = Composer(doc) + document_parts: list[DocumentPart] = [] def bc_sort_key(resource: BCBook) -> str: return resource.lang_code @@ -517,10 +499,10 @@ def rg_sort_key(resource: RGBook) -> str: bc_books = sorted(bc_books, key=bc_sort_key) rg_books = sorted(rg_books, key=rg_sort_key) for bc_book in bc_books: - subdoc = create_docx_subdoc(bc_book.book_intro, bc_book.lang_code) - composer.append(subdoc) + document_parts.append(DocumentPart(content=bc_book.book_intro)) for chapter in bc_book.chapters.values(): - subdoc = create_docx_subdoc(chapter.commentary, bc_book.lang_code) - composer.append(subdoc) - add_page_break(doc) - return composer + document_parts.append(DocumentPart(content=chapter.commentary)) + document_parts.append( + DocumentPart(content="", add_hr_p=False, add_page_break=True) + ) + return document_parts diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py index 99282119..9a9a5716 100755 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py @@ -9,19 +9,13 @@ tn_chapter_verses, tq_chapter_verses, ) -from doc.domain.assembly_strategies_docx.assembly_strategy_utils import ( - add_hr, - create_docx_subdoc, - add_one_column_section, - add_two_column_section, - add_page_break, -) from doc.domain.bible_books import BOOK_NAMES from doc.domain.model import ( AssemblyLayoutEnum, BCBook, ChunkSizeEnum, + DocumentPart, LangDirEnum, TNBook, TQBook, @@ -29,8 +23,7 @@ USFMBook, ) from doc.reviewers_guide.model import RGBook -from docx import Document # type: ignore -from docxcompose.composer import Composer # type: ignore + logger = settings.logger(__name__) @@ -47,14 +40,14 @@ def assemble_content_by_lang_then_book( assembly_layout_kind: AssemblyLayoutEnum, chunk_size: ChunkSizeEnum, book_names: Mapping[str, str] = BOOK_NAMES, -) -> Composer: +) -> list[DocumentPart]: """ Group content by language and then by book and then pass content and a couple other parameters, assembly_layout_kind and chunk_size, to interleaving strategy to do the actual interleaving. """ - composers: list[Composer] = [] + document_parts: list[DocumentPart] = [] book_id_map = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) all_lang_codes = ( {usfm_book.lang_code for usfm_book in usfm_books} @@ -127,7 +120,7 @@ def assemble_content_by_lang_then_book( ] rg_book = selected_rg_books[0] if selected_rg_books else None if usfm_book is not None: - composers.append( + document_parts.extend( assemble_usfm_by_book( usfm_book, tn_book, @@ -139,7 +132,7 @@ def assemble_content_by_lang_then_book( ) ) elif usfm_book is None and tn_book is not None: - composers.append( + document_parts.extend( assemble_tn_by_book( usfm_book, tn_book, @@ -151,7 +144,7 @@ def assemble_content_by_lang_then_book( ) ) elif usfm_book is None and tn_book is None and tq_book is not None: - composers.append( + document_parts.extend( assemble_tq_by_book( usfm_book, tn_book, @@ -168,7 +161,7 @@ def assemble_content_by_lang_then_book( and tq_book is None and (tw_book is not None or bc_book is not None or rg_book is not None) ): - composers.append( + document_parts.extend( assemble_tw_by_book( usfm_book, tn_book, @@ -179,10 +172,7 @@ def assemble_content_by_lang_then_book( rg_book, ) ) - first_composer = composers[0] - for composer in composers[1:]: - first_composer.append(composer.doc) - return first_composer + return document_parts def assemble_usfm_by_book( @@ -195,44 +185,36 @@ def assemble_usfm_by_book( rg_book: Optional[RGBook], show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, fmt_str: str = BOOK_NAME_FMT_STR, -) -> Composer: +) -> list[DocumentPart]: """ Construct the HTML for a 'by book' strategy wherein at least usfm_book_content_unit exists. """ - doc = Document() - composer = Composer(doc) + document_parts: list[DocumentPart] = [] if show_tn_book_intro and tn_book and tn_book.book_intro: - subdoc = create_docx_subdoc( - tn_book.book_intro, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tn_book.book_intro, + is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) if bc_book: if bc_book.book_intro: - subdoc = create_docx_subdoc( - bc_book.book_intro, - bc_book.lang_code, - ) - composer.append(subdoc) + document_parts.append(DocumentPart(content=bc_book.book_intro)) if usfm_book: - # fmt: off is_rtl = usfm_book and usfm_book.lang_direction == LangDirEnum.RTL - # fmt: on # Add book name - subdoc = create_docx_subdoc( - fmt_str.format(usfm_book.national_book_name), - usfm_book.lang_code, - is_rtl, - False, + document_parts.append( + DocumentPart( + content=fmt_str.format(usfm_book.national_book_name), + is_rtl=is_rtl, + add_hr_p=False, + ) ) - composer.append(subdoc) for ( chapter_num, chapter, ) in usfm_book.chapters.items(): - add_one_column_section(doc) tn_verses: str = "" tq_verses: str = "" rg_verses: str = "" @@ -247,70 +229,64 @@ def assemble_usfm_by_book( tq_verses = tq_chapter_verses(tq_book, chapter_num) if rg_book: rg_verses = rg_chapter_verses(rg_book, chapter_num) - subdoc = create_docx_subdoc( - chapter.content, - usfm_book.lang_code, - is_rtl, - ) - composer.append(subdoc) + document_parts.append(DocumentPart(content=chapter.content, is_rtl=is_rtl)) if chapter_intro_: - subdoc = create_docx_subdoc(chapter_intro_, usfm_book.lang_code, is_rtl) - composer.append(subdoc) + document_parts.append( + DocumentPart(content=chapter_intro_, is_rtl=is_rtl) + ) if chapter_commentary_: - subdoc = create_docx_subdoc( - chapter_commentary_, usfm_book.lang_code, is_rtl, False + document_parts.append( + DocumentPart( + content=chapter_commentary_, is_rtl=is_rtl, add_hr_p=False + ) ) - composer.append(subdoc) if tn_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tn_verses, - usfm_book.lang_code, - is_rtl, - False, + document_parts.append( + DocumentPart( + content=tn_verses, + is_rtl=is_rtl, + add_hr_p=False, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) - add_one_column_section(doc) - p = doc.add_paragraph() - add_hr(p) + document_parts.append(DocumentPart(content="")) if tq_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tq_verses, - usfm_book.lang_code, - is_rtl, - False, + document_parts.append( + DocumentPart( + content=tq_verses, + is_rtl=is_rtl, + add_hr_p=False, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) - add_one_column_section(doc) - p = doc.add_paragraph() - add_hr(p) + document_parts.append(DocumentPart(content="")) if rg_verses: - # add_two_column_section(doc) - subdoc = create_docx_subdoc( - rg_verses, - usfm_book.lang_code, - is_rtl, - False, + document_parts.append( + DocumentPart(content=rg_verses, is_rtl=is_rtl, add_hr_p=False) ) - composer.append(subdoc) - add_one_column_section(doc) # TODO Get feedback on whether we should allow a user to select a primary _and_ # a secondary USFM resource. If we want to limit the user to only one USFM per # document then we would want to control that in the UI and maybe also at the API # level. The API level control would be implemented in the DocumentRequest # validation. if usfm_book2: - add_one_column_section(doc) # Here we add the whole chapter's worth of verses for the secondary usfm - subdoc = create_docx_subdoc( - usfm_book2.chapters[chapter_num].content, - usfm_book.lang_code, - usfm_book2 and usfm_book2.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=usfm_book2.chapters[chapter_num].content, + is_rtl=usfm_book2 + and usfm_book2.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + ) + ) + document_parts.append( + DocumentPart( + content="", + add_hr_p=False, + add_page_break=True, ) - composer.append(subdoc) - add_page_break(doc) - return composer + ) + return document_parts def assemble_tn_by_book( @@ -322,85 +298,73 @@ def assemble_tn_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, -) -> Composer: +) -> list[DocumentPart]: """ Construct the HTML for a 'by book' strategy wherein at least tn_book exists. """ - doc = Document() - composer = Composer(doc) + document_parts: list[DocumentPart] = [] if tn_book: if show_tn_book_intro and tn_book.book_intro: - subdoc = create_docx_subdoc( - tn_book.book_intro, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tn_book.book_intro, + is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) if bc_book and bc_book.book_intro: - subdoc = create_docx_subdoc( - bc_book.book_intro, - tn_book.lang_code, - ) - composer.append(subdoc) + document_parts.append(DocumentPart(content=bc_book.book_intro)) for chapter_num in tn_book.chapters: - add_one_column_section(doc) one_column_html = [] one_column_html.append(chapter_heading(chapter_num)) one_column_html.append(chapter_intro(tn_book, chapter_num)) one_column_html_ = "".join(one_column_html) if one_column_html_: - subdoc = create_docx_subdoc( - one_column_html_, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=one_column_html_, + is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) if bc_book: - subdoc = create_docx_subdoc( - chapter_commentary(bc_book, chapter_num), - bc_book.lang_code, + document_parts.append( + DocumentPart(content=chapter_commentary(bc_book, chapter_num)) ) - composer.append(subdoc) tn_verses = tn_chapter_verses(tn_book, chapter_num) if tn_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tn_verses, - tn_book.lang_code, - tn_book and tn_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tn_verses, + is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + add_hr_p=False, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) - add_one_column_section(doc) - p = doc.add_paragraph() - add_hr(p) + document_parts.append(DocumentPart(content="")) tq_verses = tq_chapter_verses(tq_book, chapter_num) if tq_book and tq_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tq_verses, - tq_book.lang_code, - tq_book and tq_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tq_verses, + is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) - add_one_column_section(doc) - p = doc.add_paragraph() - add_hr(p) + document_parts.append(DocumentPart(content="")) rg_verses = rg_chapter_verses(rg_book, chapter_num) if rg_book and rg_verses: # add_two_column_section(doc) - subdoc = create_docx_subdoc( - rg_verses, - rg_book.lang_code, - rg_book and rg_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=rg_verses, + is_rtl=rg_book and rg_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) - # add_one_column_section(doc) - p = doc.add_paragraph() - add_hr(p) - add_page_break(doc) - - return composer + document_parts.append(DocumentPart(content="")) + document_parts.append( + DocumentPart(content="", add_hr_p=False, add_page_break=True) + ) + return document_parts def assemble_tq_by_book( @@ -411,48 +375,45 @@ def assemble_tq_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], -) -> Composer: +) -> list[DocumentPart]: """ Construct the HTML for a 'by book' strategy wherein at least tq_book exists. """ - doc = Document() - composer = Composer(doc) + document_parts: list[DocumentPart] = [] if tq_book: for chapter_num in tq_book.chapters: - add_one_column_section(doc) if bc_book: - subdoc = create_docx_subdoc( - chapter_commentary(bc_book, chapter_num), - bc_book.lang_code, + document_parts.append( + DocumentPart(content=chapter_commentary(bc_book, chapter_num)) + ) + document_parts.append( + DocumentPart( + content=chapter_heading(chapter_num), + is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, ) - composer.append(subdoc) - subdoc = create_docx_subdoc( - chapter_heading(chapter_num), - tq_book.lang_code, - tq_book and tq_book.lang_direction == LangDirEnum.RTL, ) - composer.append(subdoc) tq_verses = tq_chapter_verses(tq_book, chapter_num) if tq_verses: - add_two_column_section(doc) - subdoc = create_docx_subdoc( - tq_verses, - tq_book.lang_code, - tq_book and tq_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=tq_verses, + is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + ) ) - composer.append(subdoc) rg_verses = rg_chapter_verses(rg_book, chapter_num) if rg_book and rg_verses: - # add_two_column_section(doc) - subdoc = create_docx_subdoc( - rg_verses, - rg_book.lang_code, - rg_book and rg_book.lang_direction == LangDirEnum.RTL, + document_parts.append( + DocumentPart( + content=rg_verses, + is_rtl=rg_book and rg_book.lang_direction == LangDirEnum.RTL, + ) ) - composer.append(subdoc) - add_page_break(doc) - return composer + document_parts.append( + DocumentPart(content="", add_hr_p=False, add_page_break=True) + ) + return document_parts def assemble_tw_by_book( @@ -463,18 +424,17 @@ def assemble_tw_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], -) -> Composer: +) -> list[DocumentPart]: """ TW is handled outside this module, that is why no code for TW is explicitly included here. """ - doc = Document() - composer = Composer(doc) + document_parts: list[DocumentPart] = [] if bc_book: - subdoc = create_docx_subdoc(bc_book.book_intro, bc_book.lang_code) - composer.append(subdoc) + document_parts.append(DocumentPart(content=bc_book.book_intro)) for chapter in bc_book.chapters.values(): - subdoc = create_docx_subdoc(chapter.commentary, bc_book.lang_code) - composer.append(subdoc) - add_page_break(doc) - return composer + document_parts.append(DocumentPart(content=chapter.commentary)) + document_parts.append( + DocumentPart(content="", add_hr_p=False, add_page_break=True) + ) + return document_parts diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py index 913f70e1..5c7e54ea 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py @@ -109,26 +109,17 @@ def add_hr(paragraph: Paragraph) -> None: pBdr.append(bottom) -def create_docx_subdoc( - content: str, + + +def set_docx_language( + doc: Document, lang_code: str, - is_rtl: bool = False, - add_hr_p: bool = True, oxml_language_list_lowercase: list[str] = OXML_LANGUAGE_LIST_LOWERCASE, oxml_language_list_lowercase_split: list[str] = OXML_LANGUAGE_LIST_LOWERCASE_SPLIT, ) -> Document: - """ - Create and return a Document instance from the content parameter. - """ - html_to_docx = HtmlToDocx() - subdoc = html_to_docx.parse_html_string(content) - if is_rtl: - # Setting each run to be RTL language direction - for p in subdoc.paragraphs: - for run in p.runs: - run.font.rtl = True - if subdoc.paragraphs: - p = subdoc.paragraphs[-1] + """Set the Language for spell check""" + if doc.paragraphs: + p = doc.paragraphs[-1] # Set the language for this paragraph for the sake of the Word # spellchecker. p_run = p.add_run() @@ -175,10 +166,6 @@ def create_docx_subdoc( p_run_lang.set(qn("w:eastAsia"), "en-US") p_run_lang.set(qn("w:bidi"), "en-US") p_rpr.append(p_run_lang) - # Add a horizontal ruler at the end of the paragraph if requested. - if add_hr_p: - add_hr(p) - return subdoc def add_one_column_section(doc: Document) -> None: diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index 8735a67c..c2e8c2ab 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -24,7 +24,13 @@ from doc.domain.assembly_strategies_docx import ( assembly_strategies_lang_then_book_by_chapter as lang_then_book, ) -from doc.domain.assembly_strategies_docx.assembly_strategy_utils import add_hr +from doc.domain.assembly_strategies_docx.assembly_strategy_utils import ( + add_hr, + add_one_column_section, + add_page_break, + add_two_column_section, + set_docx_language, +) from doc.domain.bible_books import BOOK_NAMES from doc.domain.email_utils import send_email_with_attachment, should_send_email from doc.domain.model import ( @@ -33,6 +39,7 @@ Attachment, BCBook, ChunkSizeEnum, + DocumentPart, DocumentRequest, DocumentRequestSourceEnum, ResourceLookupDto, @@ -58,6 +65,7 @@ filter_unique_by_lang_code, translation_words_section, ) +from docx import Document # type: ignore from docx.enum.section import WD_SECTION # type: ignore from docxcompose.composer import Composer # type: ignore from docxtpl import DocxTemplate # type: ignore @@ -279,7 +287,7 @@ def generate_docx_document( t1 = time.time() logger.info("Time to parse all resource content: %s", t1 - t0) current_task.update_state(state="Assembling content") - composer = assemble_docx_content( + document_parts = assemble_docx_content( document_request_key_, document_request, usfm_books, @@ -303,7 +311,7 @@ def generate_docx_document( convert_html_to_docx( html_filepath_, docx_filepath_, - composer, + document_parts, document_request.layout_for_print, title1, title2, @@ -525,18 +533,18 @@ def assemble_docx_content( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], -) -> Composer: +) -> list[DocumentPart]: """ Assemble and return the content from all requested resources according to the assembly_strategy requested. """ t0 = time.time() - composer = None + document_parts: list[DocumentPart] = [] if ( document_request.assembly_strategy_kind == AssemblyStrategyEnum.LANGUAGE_BOOK_ORDER ): - composer = lang_then_book.assemble_content_by_lang_then_book( + document_parts = lang_then_book.assemble_content_by_lang_then_book( usfm_books, tn_books, tq_books, @@ -550,7 +558,7 @@ def assemble_docx_content( document_request.assembly_strategy_kind == AssemblyStrategyEnum.BOOK_LANGUAGE_ORDER ): - composer = book_then_lang.assemble_content_by_book_then_lang( + document_parts = book_then_lang.assemble_content_by_book_then_lang( usfm_books, tn_books, tq_books, @@ -562,32 +570,26 @@ def assemble_docx_content( ) t1 = time.time() logger.info("Time for interleaving document: %s", t1 - t0) - tw_subdocs = [] if tw_books: - html_to_docx = HtmlToDocx() t0 = time.time() # Add the translation words definition section for each language requested. unique_tw_books = filter_unique_by_lang_code(tw_books) for tw_book in unique_tw_books: - tw_subdoc = html_to_docx.parse_html_string( - translation_words_section( - tw_book, - usfm_books, - document_request.limit_words, - document_request.resource_requests, + document_parts.append( + DocumentPart( + content=translation_words_section( + tw_book, + usfm_books, + document_request.limit_words, + document_request.resource_requests, + ) ) ) - if tw_subdoc.paragraphs: - p = tw_subdoc.paragraphs[-1] - add_hr(p) - tw_subdocs.append(tw_subdoc) + document_parts.append(DocumentPart(content="")) t1 = time.time() logger.info("Time for adding TW content to document: %s", t1 - t0) # Now add any TW subdocs to the composer - if composer: - for tw_subdoc_ in tw_subdocs: - composer.append(tw_subdoc_) - return composer + return document_parts # HTML to PDF converters: @@ -656,18 +658,39 @@ def convert_html_to_epub( logger.info("Time for converting HTML to ePub: %s", t1 - t0) +def compose_document(document_parts: list[DocumentPart]) -> Document: + doc = Document() + html_to_docx = HtmlToDocx() + for part in document_parts: + logger.debug("part.content: %s", part.content) + if part.contained_in_two_column_section: + add_two_column_section(doc) + html_to_docx.add_html_to_document(part.content, doc) + # add_one_column_section(doc) + else: + add_one_column_section(doc) + html_to_docx.add_html_to_document(part.content, doc) + # Get spell check to behave itself + # set_docx_language(doc, lang_code) + if part.add_hr_p: + add_hr(doc.paragraphs[-1]) + if part.add_page_break: + add_page_break(doc) + return doc + + def convert_html_to_docx( html_filepath: str, docx_filepath: str, - composer: Composer, + document_parts: list[DocumentPart], layout_for_print: bool, title1: str = "title1", title2: str = "title2", - title3: str = "Formatted for Translators", + title3: str = "", docx_template_path: str = settings.DOCX_TEMPLATE_PATH, docx_compact_template_path: str = settings.DOCX_COMPACT_TEMPLATE_PATH, ) -> None: - """Generate Docx and copy it to output directory.""" + """Generate Docx and write it to output directory.""" t0 = time.time() # Get data for front page of Docx template. title1 = title1 @@ -690,8 +713,7 @@ def convert_html_to_docx( new_section = doc.add_section(WD_SECTION.CONTINUOUS) new_section.start_type master = Composer(doc) - # Add the main (non-front-matter) content. - master.append(composer.doc) + master.append(compose_document(document_parts)) master.save(docx_filepath) t1 = time.time() logger.info("Time for converting HTML to Docx: %s", t1 - t0) diff --git a/backend/doc/domain/model.py b/backend/doc/domain/model.py index c6bdaf08..0a431357 100644 --- a/backend/doc/domain/model.py +++ b/backend/doc/domain/model.py @@ -546,3 +546,11 @@ class RepoEntry(BaseModel): class SourceData(BaseModel): git_repo: list[RepoEntry] + + +class DocumentPart(BaseModel): + content: str + is_rtl: bool = False + add_hr_p: bool = True + contained_in_two_column_section: bool = False + add_page_break: bool = False From 4e8703226cc0d9226298bdedc94d8955fa37eee0 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 11 Jul 2025 19:17:30 -0700 Subject: [PATCH 103/208] Remove unnecessary source code comments --- .../assembly_strategies_book_then_lang_by_chapter.py | 7 ------- .../assembly_strategies_lang_then_book_by_chapter.py | 1 - 2 files changed, 8 deletions(-) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py index b8a977c9..da52a3ee 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py @@ -178,13 +178,11 @@ def rg_sort_key(resource: RGBook) -> str: ) ) for bc_book in bc_books: - # Add the commentary book intro document_parts.append(DocumentPart(content=bc_book.book_intro)) book_codes = {usfm_book.book_code for usfm_book in usfm_books} for book_code in book_codes: num_chapters = book_chapters[book_code] for chapter_num in range(1, num_chapters + 1): - # Add chapter intro for each language for tn_book in [ tn_book for tn_book in tn_books if tn_book.book_code == book_code ]: @@ -200,7 +198,6 @@ def rg_sort_key(resource: RGBook) -> str: bc_book for bc_book in bc_books if bc_book.book_code == book_code ]: if chapter_num in bc_book.chapters: - # Add the chapter commentary. document_parts.append( DocumentPart(content=chapter_commentary(bc_book, chapter_num)) ) @@ -216,7 +213,6 @@ def rg_sort_key(resource: RGBook) -> str: ) ) if chapter_num in usfm_book.chapters: - # Add the interleaved USFM chapters document_parts.append( DocumentPart( content=usfm_book.chapters[chapter_num].content, @@ -224,7 +220,6 @@ def rg_sort_key(resource: RGBook) -> str: and usfm_book.lang_direction == LangDirEnum.RTL, ) ) - # Add the interleaved tn notes tn_verses = None for tn_book in [ tn_book @@ -242,7 +237,6 @@ def rg_sort_key(resource: RGBook) -> str: contained_in_two_column_section=True, ) ) - # Add the interleaved tq questions for tq_book in [ tq_book for tq_book in tq_books @@ -250,7 +244,6 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tq_book.chapters: tq_verses = tq_chapter_verses(tq_book, chapter_num) - # Add TQ verse content, if any if tq_verses: document_parts.append( DocumentPart( diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py index 9a9a5716..085bbca6 100755 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py @@ -68,7 +68,6 @@ def assemble_content_by_lang_then_book( .union(rg_book.book_code for rg_book in rg_books) ) most_book_codes = list(all_book_codes) - # Cache book_id_map lookup book_codes_sorted = sorted( most_book_codes, key=lambda book_code: book_id_map[book_code] ) From 442b9119eccc106ca4e0c6308b09646dbda2b3df Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 11 Jul 2025 19:30:00 -0700 Subject: [PATCH 104/208] Fix ordering of books into canonical bible book order This impacts interleaving, but also the title strings at the top of the generated document. --- ...ly_strategies_book_then_lang_by_chapter.py | 5 ++- ...ly_strategies_lang_then_book_by_chapter.py | 4 +-- backend/doc/domain/bible_books.py | 4 +++ backend/doc/domain/document_generator.py | 36 +++++++++++++++---- backend/doc/domain/parsing.py | 8 +++-- 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py index da52a3ee..03c05f64 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py @@ -10,7 +10,7 @@ tn_chapter_verses, tq_chapter_verses, ) -from doc.domain.bible_books import BOOK_CHAPTERS, BOOK_NAMES +from doc.domain.bible_books import BOOK_CHAPTERS, BOOK_ID_MAP, BOOK_NAMES from doc.domain.model import ( AssemblyLayoutEnum, BCBook, @@ -39,6 +39,7 @@ def assemble_content_by_book_then_lang( assembly_layout_kind: AssemblyLayoutEnum, chunk_size: ChunkSizeEnum, book_names: Mapping[str, str] = BOOK_NAMES, + book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> list[DocumentPart]: """ Assemble by book then by language in alphabetic order before @@ -46,8 +47,6 @@ def assemble_content_by_book_then_lang( sub-strategy. """ document_parts: list[DocumentPart] = [] - # Sort the books in canonical order so that groupby does what we want. - book_id_map = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) most_book_codes = max( [ [usfm_book.book_code for usfm_book in usfm_books], diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py index 085bbca6..80ae41e0 100755 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py @@ -10,7 +10,7 @@ tq_chapter_verses, ) -from doc.domain.bible_books import BOOK_NAMES +from doc.domain.bible_books import BOOK_ID_MAP, BOOK_NAMES from doc.domain.model import ( AssemblyLayoutEnum, BCBook, @@ -40,6 +40,7 @@ def assemble_content_by_lang_then_book( assembly_layout_kind: AssemblyLayoutEnum, chunk_size: ChunkSizeEnum, book_names: Mapping[str, str] = BOOK_NAMES, + book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> list[DocumentPart]: """ Group content by language and then by book and then pass content @@ -48,7 +49,6 @@ def assemble_content_by_lang_then_book( interleaving. """ document_parts: list[DocumentPart] = [] - book_id_map = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) all_lang_codes = ( {usfm_book.lang_code for usfm_book in usfm_books} .union(tn_book.lang_code for tn_book in tn_books) diff --git a/backend/doc/domain/bible_books.py b/backend/doc/domain/bible_books.py index c336020d..75a2fede 100644 --- a/backend/doc/domain/bible_books.py +++ b/backend/doc/domain/bible_books.py @@ -74,6 +74,10 @@ "rev": "Revelation", } + +# Sort the books in canonical order so that groupby does what we want. +BOOK_ID_MAP = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) + BOOK_NUMBERS: Mapping[str, str] = { "gen": "01", "exo": "02", diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index c2e8c2ab..35d45a3d 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -31,7 +31,7 @@ add_two_column_section, set_docx_language, ) -from doc.domain.bible_books import BOOK_NAMES +from doc.domain.bible_books import BOOK_ID_MAP, BOOK_NAMES from doc.domain.email_utils import send_email_with_attachment, should_send_email from doc.domain.model import ( AssemblyLayoutEnum, @@ -829,18 +829,40 @@ def get_languages_title_page_strings( resource_lookup_dtos: Sequence[ResourceLookupDto], usfm_books: Sequence[USFMBook], book_names: dict[str, str] = BOOK_NAMES, + book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> tuple[str, str]: lang_codes = list({dto.lang_code for dto in resource_lookup_dtos}) def get_language_details(lang_code: str) -> str: - book_names_set = set() - resource_type_names_set = set() - dtos = [dto for dto in resource_lookup_dtos if dto.lang_code == lang_code] + book_names_ = [] + resource_type_names = [] + dtos = [ + dto + for dto in sorted( + resource_lookup_dtos, + key=lambda resource_lookup_dto: book_id_map[ + resource_lookup_dto.book_code + ], + ) + if dto.lang_code == lang_code + ] for dto in dtos: - book_names_set.add(book_names[dto.book_code]) - resource_type_names_set.add(dto.resource_type_name) + usfm_books_ = [ + usfm_book + for usfm_book in usfm_books + if usfm_book.book_code == dto.book_code + and usfm_book.lang_code == lang_code + ] + if usfm_books_: + book_name = usfm_books_[0].national_book_name + else: + book_name = book_names[dto.book_code] + if book_name not in book_names_: + book_names_.append(book_name) + if dto.resource_type_name not in resource_type_names: + resource_type_names.append(dto.resource_type_name) if dtos: - return f"{dtos[0].lang_name} ({dtos[0].localized_lang_name}): {', '.join(sorted(resource_type_names_set))} for {', '.join(sorted(book_names_set))}" + return f"{dtos[0].lang_name} ({dtos[0].localized_lang_name}): {', '.join(resource_type_names)} for {', '.join(book_names_)}" return "" lang0_title = get_language_details(lang_codes[0]) diff --git a/backend/doc/domain/parsing.py b/backend/doc/domain/parsing.py index db884fc2..d79debdf 100644 --- a/backend/doc/domain/parsing.py +++ b/backend/doc/domain/parsing.py @@ -17,7 +17,7 @@ from doc.domain.assembly_strategies.assembly_strategy_utils import ( adjust_commentary_headings, ) -from doc.domain.bible_books import BOOK_NAMES +from doc.domain.bible_books import BOOK_ID_MAP, BOOK_NAMES from doc.domain.exceptions import MissingChapterMarkerError from doc.domain.model import ( BC_RESOURCE_TYPE, @@ -905,6 +905,7 @@ def books( rg_resource_type: str = RG_RESOURCE_TYPE, docx_file_path: str = "en_rg_nt_survey.docx", en_rg_dir: str = settings.EN_RG_DIR, + book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> tuple[ Sequence[USFMBook], Sequence[TNBook], @@ -920,7 +921,10 @@ def books( bc_books = [] rg_books = [] filtered_rg_books = [] - for resource_lookup_dto, resource_dir in zip(resource_lookup_dtos, resource_dirs): + for resource_lookup_dto, resource_dir in sorted( + zip(resource_lookup_dtos, resource_dirs), + key=lambda dto_with_dir: book_id_map[dto_with_dir[0].book_code], + ): if resource_lookup_dto.resource_type in usfm_resource_types: usfm_book = usfm_book_content( resource_lookup_dto, From b4805e6a742beed3100c479547743c7c017a9223 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 11 Jul 2025 19:40:37 -0700 Subject: [PATCH 105/208] Better control over hr element insertion for docx Ground work for making the use of hr elements between sections a user-selectable option in the UI. Also important since our DocumentPart(s) interpreter now handles all hr element insertion for docx. --- ...ly_strategies_book_then_lang_by_chapter.py | 2 + ...ly_strategies_lang_then_book_by_chapter.py | 4 +- .../assembly_strategy_utils.py | 110 +++++++++++++++++- 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py index 03c05f64..1c89c9fd 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py @@ -3,6 +3,8 @@ from doc.config import settings from doc.domain.assembly_strategies.assembly_strategy_utils import ( adjust_book_intro_headings, +) +from doc.domain.assembly_strategies_docx.assembly_strategy_utils import ( bc_book_intro, chapter_commentary, chapter_intro, diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py index 80ae41e0..47b882c1 100755 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py @@ -1,9 +1,9 @@ from typing import Mapping, Optional, Sequence from doc.config import settings -from doc.domain.assembly_strategies.assembly_strategy_utils import ( +from doc.domain.assembly_strategies.assembly_strategy_utils import chapter_heading +from doc.domain.assembly_strategies_docx.assembly_strategy_utils import ( chapter_commentary, - chapter_heading, chapter_intro, rg_chapter_verses, tn_chapter_verses, diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py index 5c7e54ea..57529334 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py @@ -2,14 +2,22 @@ Utility functions used by assembly_strategies. """ +from typing import Optional + from doc.config import settings +from doc.domain.model import BCBook, TNBook, TQBook +from doc.reviewers_guide.model import RGBook +from doc.reviewers_guide.render_to_html import render_chapter from docx import Document # type: ignore from docx.enum.section import WD_SECTION # type: ignore from docx.enum.text import WD_BREAK # type: ignore from docx.oxml.ns import qn # type: ignore from docx.oxml.shared import OxmlElement # type: ignore from docx.text.paragraph import Paragraph # type: ignore -from htmldocx import HtmlToDocx # type: ignore +from doc.domain.assembly_strategies.assembly_strategy_utils import ( + TN_VERSE_NOTES_ENCLOSING_DIV_FMT_STR, + TQ_HEADING_AND_QUESTIONS_FMT_STR, +) logger = settings.logger(__name__) @@ -66,6 +74,104 @@ ] +def tn_book_intro( + tn_book: Optional[TNBook], + show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, +) -> str: + content = "" + if show_tn_book_intro and tn_book and tn_book.book_intro: + content = tn_book.book_intro + return content + + +def bc_book_intro( + bc_book: Optional[BCBook], +) -> str: + content = "" + if bc_book and bc_book.book_intro: + content = bc_book.book_intro + return content + + +def chapter_commentary( + bc_book: Optional[BCBook], + chapter_num: int, +) -> str: + """Get the chapter commentary.""" + content = "" + if ( + bc_book + and chapter_num in bc_book.chapters + and bc_book.chapters[chapter_num].commentary + ): + content = bc_book.chapters[chapter_num].commentary + return content + + +def chapter_intro( + tn_book: Optional[TNBook], + chapter_num: int, +) -> str: + """Get the chapter intro.""" + content = [] + if ( + tn_book + and chapter_num in tn_book.chapters + and tn_book.chapters[chapter_num].intro_html + ): + content.append(tn_book.chapters[chapter_num].intro_html) + return "".join(content) + + +def tn_chapter_verses( + tn_book: Optional[TNBook], + chapter_num: int, + fmt_str: str = TN_VERSE_NOTES_ENCLOSING_DIV_FMT_STR, +) -> str: + """ + Return the HTML for verses that are in the chapter with + chapter_num. + """ + content = [] + if tn_book and chapter_num in tn_book.chapters: + tn_verses = tn_book.chapters[chapter_num].verses + content.append(fmt_str.format("".join(tn_verses.values()))) + return "".join(content) + + +def tq_chapter_verses( + tq_book: Optional[TQBook], + chapter_num: int, + fmt_str: str = TQ_HEADING_AND_QUESTIONS_FMT_STR, +) -> str: + """Return the HTML for verses in chapter_num.""" + content = [] + if tq_book and chapter_num in tq_book.chapters: + tq_verses = tq_book.chapters[chapter_num].verses + content.append( + fmt_str.format( + tq_book.resource_type_name, + "".join(tq_verses.values()), + ) + ) + return "".join(content) + + +def rg_chapter_verses( + rg_book: Optional[RGBook], + chapter_num: int, +) -> str: + """ + Return the HTML for verses that are in the chapter with + chapter_num. + """ + content = [] + if rg_book and chapter_num in rg_book.chapters: + rg_verses = render_chapter(rg_book.chapters[chapter_num]) + content.append(rg_verses) + return "".join(content) + + def add_hr(paragraph: Paragraph) -> None: """Add a horizontal line at the end of the given paragraph.""" p = paragraph._p # p is the XML element @@ -109,8 +215,6 @@ def add_hr(paragraph: Paragraph) -> None: pBdr.append(bottom) - - def set_docx_language( doc: Document, lang_code: str, From 09ade4d35fba456d986b8c57b8433ce277229b46 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Sat, 12 Jul 2025 17:04:16 -0700 Subject: [PATCH 106/208] Fix issue with docx book then lang assembly --- ...ly_strategies_book_then_lang_by_chapter.py | 74 ++++++++++--------- backend/doc/domain/document_generator.py | 1 - 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py index 1c89c9fd..0222c01e 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py @@ -44,9 +44,9 @@ def assemble_content_by_book_then_lang( book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> list[DocumentPart]: """ - Assemble by book then by language in alphabetic order before - delegating more atomic ordering/interleaving to an assembly - sub-strategy. + Assemble by book in canonical bible book order then by language in + alphabetic order before delegating more atomic ordering/interleaving + to an assembly sub-strategy. """ document_parts: list[DocumentPart] = [] most_book_codes = max( @@ -83,50 +83,54 @@ def assemble_content_by_book_then_lang( rg_book for rg_book in rg_books if rg_book.book_code == book_code ] if selected_usfm_books: - document_parts = assemble_usfm_by_chapter( - usfm_books, - tn_books, - tq_books, - tw_books, - bc_books, - rg_books, + document_parts.extend( + assemble_usfm_by_chapter( + selected_usfm_books, + selected_tn_books, + selected_tq_books, + selected_tw_books, + selected_bc_books, + selected_rg_books, + ) ) - return document_parts elif not selected_usfm_books and selected_tn_books: - document_parts = assemble_tn_by_chapter( - usfm_books, - tn_books, - tq_books, - tw_books, - bc_books, - rg_books, + document_parts.extend( + assemble_tn_by_chapter( + selected_usfm_books, + selected_tn_books, + selected_tq_books, + selected_tw_books, + selected_bc_books, + selected_rg_books, + ) ) - return document_parts elif not selected_usfm_books and not selected_tn_books and selected_tq_books: - document_parts = assemble_tq_by_chapter( - usfm_books, - tn_books, - tq_books, - tw_books, - bc_books, - rg_books, + document_parts.extend( + assemble_tq_by_chapter( + selected_usfm_books, + selected_tn_books, + selected_tq_books, + selected_tw_books, + selected_bc_books, + selected_rg_books, + ) ) - return document_parts elif ( not selected_usfm_books and not selected_tn_books and not selected_tq_books and (selected_tw_books or selected_bc_books or selected_rg_books) ): - document_parts = assemble_tw_by_chapter( - usfm_books, - tn_books, - tq_books, - tw_books, - bc_books, - rg_books, + document_parts.extend( + assemble_tw_by_chapter( + selected_usfm_books, + selected_tn_books, + selected_tq_books, + selected_tw_books, + selected_bc_books, + selected_rg_books, + ) ) - return document_parts return document_parts diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index 35d45a3d..ad7f16d0 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -662,7 +662,6 @@ def compose_document(document_parts: list[DocumentPart]) -> Document: doc = Document() html_to_docx = HtmlToDocx() for part in document_parts: - logger.debug("part.content: %s", part.content) if part.contained_in_two_column_section: add_two_column_section(doc) html_to_docx.add_html_to_document(part.content, doc) From 39300a424ac11b15834e51bc90ef05f9a2434aa9 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 14 Jul 2025 14:39:58 -0700 Subject: [PATCH 107/208] Share a data structure Missed a couple spots where this change was previously made. --- .../assembly_strategies_book_then_lang_by_chapter.py | 4 ++-- .../assembly_strategies_lang_then_book_by_chapter.py | 5 ++--- backend/doc/domain/resource_lookup.py | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py index 8b4e8e81..5e5a2672 100644 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py @@ -17,7 +17,7 @@ tq_language_direction_html, usfm_language_direction_html, ) -from doc.domain.bible_books import BOOK_CHAPTERS, BOOK_NAMES +from doc.domain.bible_books import BOOK_CHAPTERS, BOOK_ID_MAP, BOOK_NAMES from doc.domain.model import ( AssemblyLayoutEnum, BCBook, @@ -50,6 +50,7 @@ def assemble_content_by_book_then_lang( rg_books: Sequence[RGBook], assembly_layout_kind: AssemblyLayoutEnum, book_names: Mapping[str, str] = BOOK_NAMES, + book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> str: """ Assemble by book then by language in alphabetic order before @@ -57,7 +58,6 @@ def assemble_content_by_book_then_lang( sub-strategy. """ content = [] - book_id_map = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) # Collect and deduplicate book codes all_book_codes = ( {usfm_book.book_code for usfm_book in usfm_books} diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py index 148a90c3..55c504df 100755 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py @@ -16,7 +16,7 @@ tq_language_direction_html, usfm_language_direction_html, ) -from doc.domain.bible_books import BOOK_NAMES +from doc.domain.bible_books import BOOK_ID_MAP, BOOK_NAMES from doc.domain.model import ( AssemblyLayoutEnum, BCBook, @@ -42,6 +42,7 @@ def assemble_content_by_lang_then_book( rg_books: Sequence[RGBook], assembly_layout_kind: AssemblyLayoutEnum, book_names: Mapping[str, str] = BOOK_NAMES, + book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> str: """ Assemble by language then by book in lexicographical order before @@ -49,8 +50,6 @@ def assemble_content_by_lang_then_book( sub-strategy. """ content = [] - # Create map for sorting books in canonical bible book order - book_id_map = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) # Collect and deduplicate language codes all_lang_codes = ( {usfm_book.lang_code for usfm_book in usfm_books} diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 7aa65a34..0506633a 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -20,7 +20,7 @@ import yaml from doc.config import settings from doc.domain import parsing, worker -from doc.domain.bible_books import BOOK_CHAPTERS, BOOK_NAMES +from doc.domain.bible_books import BOOK_CHAPTERS, BOOK_ID_MAP, BOOK_NAMES from doc.domain.model import ( NON_USFM_RESOURCE_TYPES, Content, @@ -859,6 +859,7 @@ def get_book_codes_for_lang( use_localized_book_name: bool, usfm_only: bool = False, check_usfm: bool = False, + book_id_map: dict[str, int] = BOOK_ID_MAP, download_assets: bool = settings.DOWNLOAD_ASSETS, ) -> Sequence[tuple[str, str]]: data = fetch_source_data() @@ -991,7 +992,6 @@ def get_book_codes_for_lang( unique_values = unique_tuples(book_codes_and_names) else: unique_values = unique_tuples(book_codes_and_names_localized) - book_id_map = {id: pos for pos, id in enumerate(book_names.keys())} return sorted( unique_values, key=lambda book_code_and_name: book_id_map[book_code_and_name[0]] ) From 8d7c7f8b6a19eeddc7224d76044088c409aa926f Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 14 Jul 2025 16:31:57 -0700 Subject: [PATCH 108/208] Update source code comments --- backend/doc/domain/document_generator.py | 17 +++++++---------- backend/doc/domain/parsing.py | 3 --- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index ad7f16d0..fedf3103 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -380,9 +380,9 @@ def document_request_key( else: document_request_key = f"{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}" if len(document_request_key) >= max_filename_len: - # Likely the generated filename was too long for the OS where this is - # running. In that case, use the current time as a document_request_key - # value as doing so results in an acceptably short length. + # The generated filename could be too long for the OS where this is + # running. Therefore, use the current time as a document_request_key + # so that filename is not too long. timestamp_components = str(time.time()).split(".") return f"{timestamp_components[0]}_{timestamp_components[1]}" else: @@ -588,7 +588,6 @@ def assemble_docx_content( document_parts.append(DocumentPart(content="")) t1 = time.time() logger.info("Time for adding TW content to document: %s", t1 - t0) - # Now add any TW subdocs to the composer return document_parts @@ -665,11 +664,10 @@ def compose_document(document_parts: list[DocumentPart]) -> Document: if part.contained_in_two_column_section: add_two_column_section(doc) html_to_docx.add_html_to_document(part.content, doc) - # add_one_column_section(doc) else: add_one_column_section(doc) html_to_docx.add_html_to_document(part.content, doc) - # Get spell check to behave itself + # Set the language for spellcheck # set_docx_language(doc, lang_code) if part.add_hr_p: add_hr(doc.paragraphs[-1]) @@ -695,9 +693,9 @@ def convert_html_to_docx( title1 = title1 title2 = title2 title3 = title3 - # fmt: off - template_path = docx_compact_template_path if layout_for_print else docx_template_path - # fmt: on + template_path = ( + docx_compact_template_path if layout_for_print else docx_template_path + ) doc = DocxTemplate(template_path) toc_path = generate_docx_toc(docx_filepath) toc = doc.new_subdoc(toc_path) @@ -790,7 +788,6 @@ def write_html_content_to_file( Write HTML content to file. """ logger.info("About to write HTML to %s", output_filename) - # Write the HTML file to disk. write_file( output_filename, content, diff --git a/backend/doc/domain/parsing.py b/backend/doc/domain/parsing.py index d79debdf..ec0b72ea 100644 --- a/backend/doc/domain/parsing.py +++ b/backend/doc/domain/parsing.py @@ -70,7 +70,6 @@ # fmt: on -# CHAPTER_LABEL_REGEX = r"\\cl\s+.*" CHAPTER_LABEL_REGEX = re.compile(r"\\cl\s+[^\n]+") CHAPTER_LABEL_REGEX2 = re.compile(r"\\cl\s+(.+)") CHAPTER_REGEX = re.compile(r"\\c\s+\d+") @@ -390,7 +389,6 @@ def maybe_localized_book_name(frontmatter: str) -> str: Steps 5 and 6 happen outside this function. """ - # logger.debug("frontmatter: %s", frontmatter) frontmatter_data = extract_usfm_frontmatter(frontmatter) localized_book_name = ( frontmatter_data.get("h") @@ -981,7 +979,6 @@ def ensure_paragraph_before_verses( usfm_verse_one_file_regex: str = r"^01\..*", chapter_marker_not_on_own_line_regex: str = r"^\\c [0-9]+ .*|\n", chapter_marker_not_on_own_line_with_match_groups: str = r"(^\\c [0-9]+) (.*|\n)", - # chapter_marker_not_on_own_line_repair_regex: str = r"\1\n\\p\n\2\n", chapter_marker_not_on_own_line_repair_regex: str = r"\1\n\n\2\n", ) -> str: r""" From 49ff293a39b2d9cb9559eda87ae57990267707d3 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 14 Jul 2025 16:34:15 -0700 Subject: [PATCH 109/208] Remove unused imports --- backend/doc/domain/parsing.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/doc/domain/parsing.py b/backend/doc/domain/parsing.py index ec0b72ea..aa5bc005 100644 --- a/backend/doc/domain/parsing.py +++ b/backend/doc/domain/parsing.py @@ -3,11 +3,10 @@ """ import re -import subprocess import requests import time from glob import glob -from os import DirEntry, getenv, scandir, walk +from os import DirEntry, scandir, walk from os.path import exists, join, split from pathlib import Path from typing import Mapping, Optional, Sequence From ab939761cf464b1a5ca7b2d2bc67b74290c1b021 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 15 Jul 2025 09:16:17 -0700 Subject: [PATCH 110/208] Allow user to choose section separators or not --- ...ly_strategies_book_then_lang_by_chapter.py | 119 +++++++++--- ...ly_strategies_lang_then_book_by_chapter.py | 107 +++++++--- .../assembly_strategy_utils.py | 43 ++-- ...ly_strategies_book_then_lang_by_chapter.py | 183 +++++++++++++++--- ...ly_strategies_lang_then_book_by_chapter.py | 172 +++++++++++++--- backend/doc/domain/document_generator.py | 51 +++-- backend/doc/domain/model.py | 3 + frontend/src/lib/stores/SettingsStore.ts | 1 + frontend/src/routes/books/+page.svelte | 13 +- frontend/src/routes/settings/+page.svelte | 12 +- .../routes/settings/GenerateDocument.svelte | 8 +- tests/unit/test_document_generator.py | 2 + 12 files changed, 558 insertions(+), 156 deletions(-) diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py index 5e5a2672..d6128c6d 100644 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py @@ -49,6 +49,7 @@ def assemble_content_by_book_then_lang( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], assembly_layout_kind: AssemblyLayoutEnum, + use_section_visual_separator: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> str: @@ -103,6 +104,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) elif ( @@ -121,6 +123,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) elif ( @@ -140,6 +143,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) elif ( @@ -160,6 +164,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) elif selected_usfm_books and ( @@ -176,6 +181,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) return "".join(content) @@ -188,6 +194,7 @@ def assemble_usfm_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", hr: str = "


", @@ -225,12 +232,12 @@ def rg_sort_key(resource: RGBook) -> str: if show_tn_book_intro: for tn_book in tn_books: content.append(tn_language_direction_html(tn_book)) - book_intro_ = tn_book_intro(tn_book) + book_intro_ = tn_book_intro(tn_book, use_section_visual_separator) book_intro_adj = adjust_book_intro_headings(book_intro_) content.append(book_intro_adj) content.append(close_direction_html) for bc_book in bc_books: - content.append(bc_book_intro(bc_book)) + content.append(bc_book_intro(bc_book, use_section_visual_separator)) book_codes = {usfm_book.book_code for usfm_book in usfm_books} for book_code in book_codes: num_chapters = book_chapters[book_code] @@ -240,13 +247,21 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tn_book.chapters: content.append(tn_language_direction_html(tn_book)) - content.append(chapter_intro(tn_book, chapter_num)) + content.append( + chapter_intro( + tn_book, chapter_num, use_section_visual_separator + ) + ) content.append(close_direction_html) for bc_book in [ bc_book for bc_book in bc_books if bc_book.book_code == book_code ]: if chapter_num in bc_book.chapters: - content.append(chapter_commentary(bc_book, chapter_num)) + content.append( + chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ) + ) for usfm_book in [ usfm_book for usfm_book in usfm_books @@ -266,7 +281,9 @@ def rg_sort_key(resource: RGBook) -> str: tn_book for tn_book in tn_books if tn_book.book_code == book_code ]: if chapter_num in tn_book.chapters: - tn_verses = tn_chapter_verses(tn_book, chapter_num) + tn_verses = tn_chapter_verses( + tn_book, chapter_num, use_section_visual_separator + ) if tn_verses: content.append(tn_language_direction_html(tn_book)) content.append(tn_verses) @@ -276,7 +293,9 @@ def rg_sort_key(resource: RGBook) -> str: tq_book for tq_book in tq_books if tq_book.book_code == book_code ]: if chapter_num in tq_book.chapters: - tq_verses = tq_chapter_verses(tq_book, chapter_num) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) if tq_verses: content.append(tq_language_direction_html(tq_book)) content.append(tq_verses) @@ -290,7 +309,9 @@ def rg_sort_key(resource: RGBook) -> str: # and rg_book.book_code == book_code ]: if chapter_num in rg_book.chapters: - rg_verses = rg_chapter_verses(rg_book, chapter_num) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) if rg_verses: content.append(rg_language_direction_html(rg_book)) content.append(rg_verses) @@ -306,6 +327,7 @@ def assemble_tn_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, @@ -334,56 +356,65 @@ def rg_sort_key(resource: RGBook) -> str: bc_books = sorted(bc_books, key=bc_sort_key) rg_books = sorted(rg_books, key=rg_sort_key) if show_tn_book_intro: - # Add book intros for each tn_book for tn_book in tn_books: content.append(tn_language_direction_html(tn_book)) - book_intro_ = tn_book_intro(tn_book) + book_intro_ = tn_book_intro(tn_book, use_section_visual_separator) book_intro_adj = adjust_book_intro_headings(book_intro_) content.append(book_intro_adj) content.append(close_direction_html) for bc_book in bc_books: - content.append(bc_book_intro(bc_book)) + content.append(bc_book_intro(bc_book, use_section_visual_separator)) book_codes = {tn_book.book_code for tn_book in tn_books} for book_code in book_codes: num_chapters = book_chapters[book_code] for chapter_num in range(1, num_chapters + 1): - # Add chapter intro for tn_book in [ tn_book for tn_book in tn_books if tn_book.book_code == book_code ]: if chapter_num in tn_book.chapters: content.append(tn_language_direction_html(tn_book)) - content.append(chapter_intro(tn_book, chapter_num)) + content.append( + chapter_intro( + tn_book, chapter_num, use_section_visual_separator + ) + ) content.append(close_direction_html) for bc_book in [ bc_book for bc_book in bc_books if bc_book.book_code == book_code ]: if chapter_num in bc_book.chapters: - content.append(chapter_commentary(bc_book, chapter_num)) - # Add tn notes + content.append( + chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ) + ) for tn_book in [ tn_book for tn_book in tn_books if tn_book.book_code == book_code ]: if chapter_num in tn_book.chapters: - tn_verses = tn_chapter_verses(tn_book, chapter_num) + tn_verses = tn_chapter_verses( + tn_book, chapter_num, use_section_visual_separator + ) content.append(tn_language_direction_html(tn_book)) content.append(tn_verses) content.append(close_direction_html) - # Add tq questions for tq_book in [ tq_book for tq_book in tq_books if tq_book.book_code == book_code ]: if chapter_num in tq_book.chapters: - tq_verses = tq_chapter_verses(tq_book, chapter_num) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) content.append(tq_language_direction_html(tq_book)) content.append(tq_verses) content.append(close_direction_html) - # Add rg content for rg_book in [ rg_book for rg_book in rg_books if rg_book.book_code == book_code ]: if chapter_num in rg_book.chapters: - rg_verses = rg_chapter_verses(rg_book, chapter_num) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) if rg_verses: content.append(rg_language_direction_html(rg_book)) content.append(rg_verses) @@ -399,6 +430,7 @@ def assemble_tq_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, @@ -430,12 +462,18 @@ def rg_sort_key(resource: RGBook) -> str: bc_book for bc_book in bc_books if bc_book.book_code == book_code ]: if chapter_num in bc_book.chapters: - content.append(chapter_commentary(bc_book, chapter_num)) + content.append( + chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ) + ) for tq_book in [ tq_book for tq_book in tq_books if tq_book.book_code == book_code ]: if chapter_num in tq_book.chapters: - tq_verses = tq_chapter_verses(tq_book, chapter_num) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) if tq_verses: content.append(tq_language_direction_html(tq_book)) content.append(tq_verses) @@ -444,7 +482,9 @@ def rg_sort_key(resource: RGBook) -> str: rg_book for rg_book in rg_books if rg_book.book_code == book_code ]: if chapter_num in rg_book.chapters: - rg_verses = rg_chapter_verses(rg_book, chapter_num) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) if rg_verses: content.append(rg_language_direction_html(rg_book)) content.append(rg_verses) @@ -467,6 +507,7 @@ def assemble_tw_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, ) -> str: content = [] @@ -476,9 +517,11 @@ def bc_sort_key(resource: BCBook) -> str: bc_books = sorted(bc_books, key=bc_sort_key) for bc_book in bc_books: - content.append(bc_book_intro(bc_book)) + content.append(bc_book_intro(bc_book, use_section_visual_separator)) for chapter_num, chapter in bc_book.chapters.items(): - content.append(chapter_commentary(bc_book, chapter_num)) + content.append( + chapter_commentary(bc_book, chapter_num, use_section_visual_separator) + ) content.append(end_of_chapter_html) return "".join(content) @@ -490,6 +533,7 @@ def assemble_usfm_by_chapter_2c_sl_sr( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, html_row_begin: str = HTML_ROW_BEGIN, html_column_begin: str = HTML_COLUMN_BEGIN, html_column_left_begin: str = HTML_COLUMN_LEFT_BEGIN, @@ -637,8 +681,7 @@ def rg_sort_key(resource: RGBook) -> str: content.append(adjust_book_intro_headings(book_intro_)) content.append(close_direction_html) for bc_book in bc_books: - content.append(bc_book_intro(bc_book)) - # Get unique book codes in usfm_books + content.append(bc_book_intro(bc_book, use_section_visual_separator)) for usfm_book in usfm_books: for chapter_num, chapter in usfm_book.chapters.items(): content.append(fmt_str.format(usfm_book.national_book_name)) @@ -649,7 +692,11 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tn_book.chapters: content.append(tn_language_direction_html(tn_book)) - content.append(chapter_intro(tn_book, chapter_num)) + content.append( + chapter_intro( + tn_book, chapter_num, use_section_visual_separator + ) + ) content.append(close_direction_html) for bc_book in [ bc_book @@ -657,7 +704,11 @@ def rg_sort_key(resource: RGBook) -> str: if bc_book.book_code == usfm_book.book_code ]: if chapter_num in bc_book.chapters: - content.append(chapter_commentary(bc_book, chapter_num)) + content.append( + chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ) + ) # Get lang_code of first USFM so that we can use it later # to make sure USFMs of the same language are on the same # side of the two column layout. @@ -693,7 +744,9 @@ def rg_sort_key(resource: RGBook) -> str: # notes on the left and lang1 notes on the right. tn_verses = None for idx, tn_book in enumerate(tn_books): - tn_verses = tn_chapter_verses(tn_book, chapter_num) + tn_verses = tn_chapter_verses( + tn_book, chapter_num, use_section_visual_separator + ) if tn_verses: if is_even(idx): content.append(html_row_begin) @@ -707,7 +760,9 @@ def rg_sort_key(resource: RGBook) -> str: # questions on the left and lang1 questions on the right. tq_verses = None for idx, tq_book in enumerate(tq_books): - tq_verses = tq_chapter_verses(tq_book, chapter_num) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) if tq_verses: if is_even(idx): content.append(html_row_begin) @@ -723,7 +778,9 @@ def rg_sort_key(resource: RGBook) -> str: if rg_book.book_code == usfm_book.book_code ] ): - rg_verses = rg_chapter_verses(rg_book, chapter_num) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) if rg_verses: if is_even(idx): content.append(html_row_begin) diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py index 55c504df..20847abf 100755 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py @@ -41,6 +41,7 @@ def assemble_content_by_lang_then_book( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], assembly_layout_kind: AssemblyLayoutEnum, + use_section_visual_separator: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> str: @@ -70,11 +71,9 @@ def assemble_content_by_lang_then_book( .union(rg_book.book_code for rg_book in rg_books) ) book_codes = list(all_book_codes) - # Cache book_id_map lookup book_codes_sorted = sorted(book_codes, key=lambda book_code: book_id_map[book_code]) for lang_code in lang_codes: for book_code in book_codes_sorted: - # logger.debug("lang_code: %s, book_code: %s", lang_code, book_code) selected_usfm_books = [ usfm_book for usfm_book in usfm_books @@ -126,6 +125,7 @@ def assemble_content_by_lang_then_book( usfm_book2, bc_book, rg_book, + use_section_visual_separator, ) ) elif usfm_book is None and tn_book is not None: @@ -138,6 +138,7 @@ def assemble_content_by_lang_then_book( usfm_book2, bc_book, rg_book, + use_section_visual_separator, ) ) elif usfm_book is None and tn_book is None and tq_book is not None: @@ -150,6 +151,7 @@ def assemble_content_by_lang_then_book( usfm_book2, bc_book, rg_book, + use_section_visual_separator, ) ) elif ( @@ -167,6 +169,7 @@ def assemble_content_by_lang_then_book( usfm_book2, bc_book, rg_book, + use_section_visual_separator, ) ) return "".join(content) @@ -180,6 +183,7 @@ def assemble_usfm_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, hr: str = "
", close_direction_html: str = "", @@ -187,29 +191,42 @@ def assemble_usfm_by_book( ) -> str: content = [] content.append(usfm_language_direction_html(usfm_book)) - content.append(tn_book_intro(tn_book)) - content.append(bc_book_intro(bc_book)) + content.append(tn_book_intro(tn_book, use_section_visual_separator)) + content.append(bc_book_intro(bc_book, use_section_visual_separator)) if usfm_book: - # Add book name content.append(fmt_str.format(usfm_book.national_book_name)) for ( chapter_num, chapter, ) in usfm_book.chapters.items(): content.append(chapter.content) - if not has_footnotes(chapter.content) and ( - usfm_book2 is not None - or tn_book is not None - or tq_book is not None - or rg_book is not None - or tw_book is not None + if ( + not has_footnotes(chapter.content) + and ( + usfm_book2 is not None + or tn_book is not None + or tq_book is not None + or rg_book is not None + or tw_book is not None + ) + and use_section_visual_separator ): content.append(hr) - content.append(chapter_intro(tn_book, chapter_num)) - content.append(chapter_commentary(bc_book, chapter_num)) - content.append(tn_chapter_verses(tn_book, chapter_num)) - content.append(tq_chapter_verses(tq_book, chapter_num)) - content.append(rg_chapter_verses(rg_book, chapter_num)) + content.append( + chapter_intro(tn_book, chapter_num, use_section_visual_separator) + ) + content.append( + chapter_commentary(bc_book, chapter_num, use_section_visual_separator) + ) + content.append( + tn_chapter_verses(tn_book, chapter_num, use_section_visual_separator) + ) + content.append( + tq_chapter_verses(tq_book, chapter_num, use_section_visual_separator) + ) + content.append( + rg_chapter_verses(rg_book, chapter_num, use_section_visual_separator) + ) # If the user chose two USFM resource types for a language. e.g., fr: # ulb, f10, show the second USFM content here if usfm_book2: @@ -228,20 +245,31 @@ def assemble_tn_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> str: content = [] content.append(tn_language_direction_html(tn_book)) - content.append(tn_book_intro(tn_book)) + content.append(tn_book_intro(tn_book, use_section_visual_separator)) if tn_book: for chapter_num in tn_book.chapters: content.append(chapter_heading(chapter_num)) - content.append(chapter_intro(tn_book, chapter_num)) - content.append(chapter_commentary(bc_book, chapter_num)) - content.append(tn_chapter_verses(tn_book, chapter_num)) - content.append(tq_chapter_verses(tq_book, chapter_num)) - content.append(rg_chapter_verses(rg_book, chapter_num)) + content.append( + chapter_intro(tn_book, chapter_num, use_section_visual_separator) + ) + content.append( + chapter_commentary(bc_book, chapter_num, use_section_visual_separator) + ) + content.append( + tn_chapter_verses(tn_book, chapter_num, use_section_visual_separator) + ) + content.append( + tq_chapter_verses(tq_book, chapter_num, use_section_visual_separator) + ) + content.append( + rg_chapter_verses(rg_book, chapter_num, use_section_visual_separator) + ) content.append(end_of_chapter_html) content.append(close_direction_html) return "".join(content) @@ -255,6 +283,7 @@ def assemble_tq_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> str: @@ -262,10 +291,16 @@ def assemble_tq_by_book( content.append(tq_language_direction_html(tq_book)) if tq_book: for chapter_num in tq_book.chapters: - content.append(chapter_commentary(bc_book, chapter_num)) + content.append( + chapter_commentary(bc_book, chapter_num, use_section_visual_separator) + ) content.append(chapter_heading(chapter_num)) - content.append(tq_chapter_verses(tq_book, chapter_num)) - content.append(rg_chapter_verses(rg_book, chapter_num)) + content.append( + tq_chapter_verses(tq_book, chapter_num, use_section_visual_separator) + ) + content.append( + rg_chapter_verses(rg_book, chapter_num, use_section_visual_separator) + ) content.append(end_of_chapter_html) content.append(close_direction_html) return "".join(content) @@ -278,6 +313,7 @@ def assemble_rg_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> str: @@ -305,7 +341,11 @@ def rg_sort_key(resource: RGBook) -> str: and bc_book.book_code == rg_book_.book_code ]: if chapter_num in bc_book.chapters: - content.append(chapter_commentary(bc_book, chapter_num)) + content.append( + chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ) + ) for rg_book in [ rg_book for rg_book in rg_books @@ -314,7 +354,11 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in rg_book.chapters: content.append(rg_language_direction_html(rg_book)) - content.append(rg_chapter_verses(rg_book, chapter_num)) + content.append( + rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) + ) content.append(close_direction_html) return "".join(content) @@ -329,17 +373,22 @@ def assemble_tw_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], + use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> str: content = [] if bc_book: for chapter_num in bc_book.chapters: - content.append(chapter_commentary(bc_book, chapter_num)) + content.append( + chapter_commentary(bc_book, chapter_num, use_section_visual_separator) + ) content.append(end_of_chapter_html) if rg_book: for chapter_num in rg_book.chapters: - content.append(rg_chapter_verses(rg_book, chapter_num)) + content.append( + rg_chapter_verses(rg_book, chapter_num, use_section_visual_separator) + ) content.append(end_of_chapter_html) return "".join(content) diff --git a/backend/doc/domain/assembly_strategies/assembly_strategy_utils.py b/backend/doc/domain/assembly_strategies/assembly_strategy_utils.py index 5475bb69..f10ffa6c 100755 --- a/backend/doc/domain/assembly_strategies/assembly_strategy_utils.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategy_utils.py @@ -67,6 +67,7 @@ def adjust_commentary_headings( def chapter_intro( tn_book: Optional[TNBook], chapter_num: int, + use_section_visual_separator: bool, hr: str = "
", ) -> str: """Get the chapter intro.""" @@ -77,7 +78,8 @@ def chapter_intro( and tn_book.chapters[chapter_num].intro_html ): content.append(tn_book.chapters[chapter_num].intro_html) - content.append(hr) + if use_section_visual_separator: + content.append(hr) return "".join(content) @@ -87,39 +89,48 @@ def has_footnotes(html_content: str) -> bool: def bc_book_intro( bc_book: Optional[BCBook], + use_section_visual_separator: bool, hr: str = "
", ) -> str: - content = "" + content = [] if bc_book and bc_book.book_intro: - content = f"{bc_book.book_intro}{hr}" - return content + content.append(bc_book.book_intro) + if use_section_visual_separator: + content.append(hr) + return "".join(content) def tn_book_intro( tn_book: Optional[TNBook], + use_section_visual_separator: bool, hr: str = "
", show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, ) -> str: - content = "" + content = [] if show_tn_book_intro and tn_book and tn_book.book_intro: - content = f"{tn_book.book_intro}{hr}" - return content + content.append(tn_book.book_intro) + if use_section_visual_separator: + content.append(hr) + return "".join(content) def chapter_commentary( bc_book: Optional[BCBook], chapter_num: int, + use_section_visual_separator: bool, hr: str = "
", ) -> str: """Get the chapter commentary.""" - content = "" + content = [] if ( bc_book and chapter_num in bc_book.chapters and bc_book.chapters[chapter_num].commentary ): - content = f"{bc_book.chapters[chapter_num].commentary}{hr}" - return content + content.append(bc_book.chapters[chapter_num].commentary) + if use_section_visual_separator: + content.append(hr) + return "".join(content) def usfm_language_direction_html( @@ -169,6 +180,7 @@ def rg_language_direction_html( def tn_chapter_verses( tn_book: Optional[TNBook], chapter_num: int, + use_section_visual_separator: bool, fmt_str: str = TN_VERSE_NOTES_ENCLOSING_DIV_FMT_STR, hr: str = "
", ) -> str: @@ -180,13 +192,15 @@ def tn_chapter_verses( if tn_book and chapter_num in tn_book.chapters: tn_verses = tn_book.chapters[chapter_num].verses content.append(fmt_str.format("".join(tn_verses.values()))) - content.append(hr) + if use_section_visual_separator: + content.append(hr) return "".join(content) def tq_chapter_verses( tq_book: Optional[TQBook], chapter_num: int, + use_section_visual_separator: bool, fmt_str: str = TQ_HEADING_AND_QUESTIONS_FMT_STR, hr: str = "
", ) -> str: @@ -200,13 +214,15 @@ def tq_chapter_verses( "".join(tq_verses.values()), ) ) - content.append(hr) + if use_section_visual_separator: + content.append(hr) return "".join(content) def rg_chapter_verses( rg_book: Optional[RGBook], chapter_num: int, + use_section_visual_separator: bool, hr: str = "
", ) -> str: """ @@ -217,7 +233,8 @@ def rg_chapter_verses( if rg_book and chapter_num in rg_book.chapters: rg_verses = render_chapter(rg_book.chapters[chapter_num]) content.append(rg_verses) - content.append(hr) + if use_section_visual_separator: + content.append(hr) return "".join(content) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py index 0222c01e..7a8b092d 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py @@ -3,8 +3,6 @@ from doc.config import settings from doc.domain.assembly_strategies.assembly_strategy_utils import ( adjust_book_intro_headings, -) -from doc.domain.assembly_strategies_docx.assembly_strategy_utils import ( bc_book_intro, chapter_commentary, chapter_intro, @@ -26,6 +24,7 @@ ) from doc.reviewers_guide.model import RGBook + logger = settings.logger(__name__) BOOK_NAME_FMT_STR: str = "

{}

" @@ -40,6 +39,7 @@ def assemble_content_by_book_then_lang( rg_books: Sequence[RGBook], assembly_layout_kind: AssemblyLayoutEnum, chunk_size: ChunkSizeEnum, + use_section_visual_separator: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> list[DocumentPart]: @@ -91,6 +91,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) elif not selected_usfm_books and selected_tn_books: @@ -102,6 +103,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) elif not selected_usfm_books and not selected_tn_books and selected_tq_books: @@ -113,6 +115,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) elif ( @@ -129,6 +132,7 @@ def assemble_content_by_book_then_lang( selected_tw_books, selected_bc_books, selected_rg_books, + use_section_visual_separator, ) ) return document_parts @@ -141,6 +145,7 @@ def assemble_usfm_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, fmt_str: str = BOOK_NAME_FMT_STR, @@ -180,10 +185,16 @@ def rg_sort_key(resource: RGBook) -> str: DocumentPart( content=book_intro_adj, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) for bc_book in bc_books: - document_parts.append(DocumentPart(content=bc_book.book_intro)) + document_parts.append( + DocumentPart( + content=bc_book.book_intro, + use_section_visual_separator=use_section_visual_separator, + ) + ) book_codes = {usfm_book.book_code for usfm_book in usfm_books} for book_code in book_codes: num_chapters = book_chapters[book_code] @@ -194,9 +205,12 @@ def rg_sort_key(resource: RGBook) -> str: if chapter_num in tn_book.chapters: document_parts.append( DocumentPart( - content=chapter_intro(tn_book, chapter_num), + content=chapter_intro( + tn_book, chapter_num, use_section_visual_separator + ), is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) for bc_book in [ @@ -204,7 +218,12 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in bc_book.chapters: document_parts.append( - DocumentPart(content=chapter_commentary(bc_book, chapter_num)) + DocumentPart( + content=chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ), + use_section_visual_separator=use_section_visual_separator, + ) ) for usfm_book in [ usfm_book @@ -215,6 +234,7 @@ def rg_sort_key(resource: RGBook) -> str: DocumentPart( content=fmt_str.format(usfm_book.national_book_name), add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, ) ) if chapter_num in usfm_book.chapters: @@ -223,6 +243,7 @@ def rg_sort_key(resource: RGBook) -> str: content=usfm_book.chapters[chapter_num].content, is_rtl=usfm_book and usfm_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) tn_verses = None @@ -232,7 +253,9 @@ def rg_sort_key(resource: RGBook) -> str: if tn_book.book_code == usfm_book.book_code ]: if chapter_num in tn_book.chapters: - tn_verses = tn_chapter_verses(tn_book, chapter_num) + tn_verses = tn_chapter_verses( + tn_book, chapter_num, use_section_visual_separator + ) if tn_verses: document_parts.append( DocumentPart( @@ -240,6 +263,14 @@ def rg_sort_key(resource: RGBook) -> str: is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, contained_in_two_column_section=True, + add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=use_section_visual_separator, ) ) for tq_book in [ @@ -248,7 +279,9 @@ def rg_sort_key(resource: RGBook) -> str: if tq_book.book_code == usfm_book.book_code ]: if chapter_num in tq_book.chapters: - tq_verses = tq_chapter_verses(tq_book, chapter_num) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) if tq_verses: document_parts.append( DocumentPart( @@ -256,6 +289,14 @@ def rg_sort_key(resource: RGBook) -> str: is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, contained_in_two_column_section=True, + add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=use_section_visual_separator, ) ) for rg_book in [ @@ -265,10 +306,20 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in rg_book.chapters: document_parts.append( - DocumentPart(content=rg_chapter_verses(rg_book, chapter_num)) + DocumentPart( + content=rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ), + use_section_visual_separator=use_section_visual_separator, + ), ) document_parts.append( - DocumentPart(content="", add_hr_p=False, add_page_break=True) + DocumentPart( + content="", + add_hr_p=False, + add_page_break=True, + use_section_visual_separator=use_section_visual_separator, + ) ) return document_parts @@ -280,6 +331,7 @@ def assemble_tn_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, ) -> list[DocumentPart]: @@ -315,10 +367,16 @@ def rg_sort_key(resource: RGBook) -> str: DocumentPart( content=book_intro_adj, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) for bc_book in bc_books: - document_parts.append(DocumentPart(content=bc_book_intro(bc_book))) + document_parts.append( + DocumentPart( + content=bc_book_intro(bc_book, use_section_visual_separator), + use_section_visual_separator=use_section_visual_separator, + ) + ) book_codes = {tn_book.book_code for tn_book in tn_books} for book_code in book_codes: num_chapters = book_chapters[book_code] @@ -327,10 +385,12 @@ def rg_sort_key(resource: RGBook) -> str: tn_book for tn_book in tn_books if tn_book.book_code == book_code ]: one_column_html = [] - # one_column_html.append("Chapter {}".format(chapter_num)) if chapter_num in tn_book.chapters: - # Add the translation notes chapter intro. - one_column_html.append(chapter_intro(tn_book, chapter_num)) + one_column_html.append( + chapter_intro( + tn_book, chapter_num, use_section_visual_separator + ) + ) one_column_html_ = "".join(one_column_html) if one_column_html_: document_parts.append( @@ -338,36 +398,51 @@ def rg_sort_key(resource: RGBook) -> str: content=one_column_html_, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) for bc_book in [ bc_book for bc_book in bc_books if bc_book.book_code == book_code ]: if chapter_num in bc_book.chapters: - # Add the chapter commentary. document_parts.append( - DocumentPart(content=chapter_commentary(bc_book, chapter_num)) + DocumentPart( + content=chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ), + use_section_visual_separator=use_section_visual_separator, + ) ) - # Add the interleaved tn notes for tn_book in [ tn_book for tn_book in tn_books if tn_book.book_code == book_code ]: if chapter_num in tn_book.chapters: - tn_verses = tn_chapter_verses(tn_book, chapter_num) + tn_verses = tn_chapter_verses( + tn_book, chapter_num, use_section_visual_separator + ) if tn_verses: document_parts.append( DocumentPart( content=tn_verses, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=use_section_visual_separator, ) ) - # Add the interleaved tq questions for tq_book in [ tq_book for tq_book in tq_books if tq_book.book_code == book_code ]: - tq_verses = tq_chapter_verses(tq_book, chapter_num) - # Add TQ verse content, if any + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) if tq_verses: document_parts.append( DocumentPart( @@ -375,22 +450,40 @@ def rg_sort_key(resource: RGBook) -> str: is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, contained_in_two_column_section=True, + add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=use_section_visual_separator, ) ) for rg_book in [ rg_book for rg_book in rg_books if rg_book.book_code == book_code ]: - rg_verses = rg_chapter_verses(rg_book, chapter_num) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) if rg_verses: document_parts.append( DocumentPart( content=rg_verses, is_rtl=rg_book and rg_book.lang_direction == LangDirEnum.RTL, + contained_in_two_column_section=True, + add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, ) ) document_parts.append( - DocumentPart(content="", add_hr_p=False, add_page_break=True) + DocumentPart( + content="", + add_hr_p=False, + add_page_break=True, + use_section_visual_separator=use_section_visual_separator, + ) ) return document_parts @@ -402,6 +495,7 @@ def assemble_tq_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, book_chapters: Mapping[str, int] = BOOK_CHAPTERS, ) -> list[DocumentPart]: """ @@ -431,16 +525,21 @@ def rg_sort_key(resource: RGBook) -> str: for bc_book in [ bc_book for bc_book in bc_books if bc_book.book_code == book_code ]: - one_column_html.append(chapter_commentary(bc_book, chapter_num)) + one_column_html.append( + chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ) + ) if one_column_html: document_parts.append(DocumentPart(content="".join(one_column_html))) - # Add the interleaved tq questions for tq_book in [ tq_book for tq_book in tq_books if tq_book.book_code == tq_book.book_code ]: - tq_verses = tq_chapter_verses(tq_book, chapter_num) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) if tq_verses: document_parts.append( DocumentPart( @@ -448,6 +547,14 @@ def rg_sort_key(resource: RGBook) -> str: is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, contained_in_two_column_section=True, + add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=use_section_visual_separator, ) ) for rg_book in [ @@ -455,7 +562,9 @@ def rg_sort_key(resource: RGBook) -> str: for rg_book in rg_books if rg_book.book_code == rg_book.book_code ]: - rg_verses = rg_chapter_verses(rg_book, chapter_num) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) if rg_verses: document_parts.append( DocumentPart( @@ -484,6 +593,7 @@ def assemble_tw_by_chapter( tw_books: Sequence[TWBook], bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], + use_section_visual_separator: bool, ) -> list[DocumentPart]: """Construct the HTML for BC and TW.""" document_parts: list[DocumentPart] = [] @@ -497,10 +607,25 @@ def rg_sort_key(resource: RGBook) -> str: bc_books = sorted(bc_books, key=bc_sort_key) rg_books = sorted(rg_books, key=rg_sort_key) for bc_book in bc_books: - document_parts.append(DocumentPart(content=bc_book.book_intro)) + document_parts.append( + DocumentPart( + content=bc_book.book_intro, + use_section_visual_separator=use_section_visual_separator, + ) + ) for chapter in bc_book.chapters.values(): - document_parts.append(DocumentPart(content=chapter.commentary)) document_parts.append( - DocumentPart(content="", add_hr_p=False, add_page_break=True) + DocumentPart( + content=chapter.commentary, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + add_hr_p=False, + add_page_break=True, + use_section_visual_separator=use_section_visual_separator, + ) ) return document_parts diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py index 47b882c1..09164316 100755 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py @@ -1,8 +1,8 @@ from typing import Mapping, Optional, Sequence from doc.config import settings -from doc.domain.assembly_strategies.assembly_strategy_utils import chapter_heading -from doc.domain.assembly_strategies_docx.assembly_strategy_utils import ( +from doc.domain.assembly_strategies.assembly_strategy_utils import ( + chapter_heading, chapter_commentary, chapter_intro, rg_chapter_verses, @@ -39,6 +39,7 @@ def assemble_content_by_lang_then_book( rg_books: Sequence[RGBook], assembly_layout_kind: AssemblyLayoutEnum, chunk_size: ChunkSizeEnum, + use_section_visual_separator: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> list[DocumentPart]: @@ -128,6 +129,7 @@ def assemble_content_by_lang_then_book( usfm_book2, bc_book, rg_book, + use_section_visual_separator, ) ) elif usfm_book is None and tn_book is not None: @@ -140,6 +142,7 @@ def assemble_content_by_lang_then_book( usfm_book2, bc_book, rg_book, + use_section_visual_separator, ) ) elif usfm_book is None and tn_book is None and tq_book is not None: @@ -152,6 +155,7 @@ def assemble_content_by_lang_then_book( usfm_book2, bc_book, rg_book, + use_section_visual_separator, ) ) elif ( @@ -169,6 +173,7 @@ def assemble_content_by_lang_then_book( usfm_book2, bc_book, rg_book, + use_section_visual_separator, ) ) return document_parts @@ -182,6 +187,7 @@ def assemble_usfm_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], + use_section_visual_separator: bool, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, fmt_str: str = BOOK_NAME_FMT_STR, ) -> list[DocumentPart]: @@ -195,11 +201,17 @@ def assemble_usfm_by_book( DocumentPart( content=tn_book.book_intro, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) if bc_book: if bc_book.book_intro: - document_parts.append(DocumentPart(content=bc_book.book_intro)) + document_parts.append( + DocumentPart( + content=bc_book.book_intro, + use_section_visual_separator=use_section_visual_separator, + ) + ) if usfm_book: is_rtl = usfm_book and usfm_book.lang_direction == LangDirEnum.RTL # Add book name @@ -208,6 +220,7 @@ def assemble_usfm_by_book( content=fmt_str.format(usfm_book.national_book_name), is_rtl=is_rtl, add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, ) ) for ( @@ -219,24 +232,43 @@ def assemble_usfm_by_book( rg_verses: str = "" chapter_intro_ = "" chapter_commentary_ = "" - if tn_book: - chapter_intro_ = chapter_intro(tn_book, chapter_num) - tn_verses = tn_chapter_verses(tn_book, chapter_num) - if bc_book: - chapter_commentary_ = chapter_commentary(bc_book, chapter_num) - if tq_book: - tq_verses = tq_chapter_verses(tq_book, chapter_num) - if rg_book: - rg_verses = rg_chapter_verses(rg_book, chapter_num) - document_parts.append(DocumentPart(content=chapter.content, is_rtl=is_rtl)) + chapter_intro_ = chapter_intro( + tn_book, chapter_num, use_section_visual_separator + ) + tn_verses = tn_chapter_verses( + tn_book, chapter_num, use_section_visual_separator + ) + chapter_commentary_ = chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) + document_parts.append( + DocumentPart( + content=chapter.content, + is_rtl=is_rtl, + use_section_visual_separator=use_section_visual_separator, + ) + ) if chapter_intro_: document_parts.append( - DocumentPart(content=chapter_intro_, is_rtl=is_rtl) + DocumentPart( + content=chapter_intro_, + is_rtl=is_rtl, + use_section_visual_separator=use_section_visual_separator, + ) ) if chapter_commentary_: document_parts.append( DocumentPart( - content=chapter_commentary_, is_rtl=is_rtl, add_hr_p=False + content=chapter_commentary_, + is_rtl=is_rtl, + add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, ) ) if tn_verses: @@ -246,9 +278,15 @@ def assemble_usfm_by_book( is_rtl=is_rtl, add_hr_p=False, contained_in_two_column_section=True, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=use_section_visual_separator, ) ) - document_parts.append(DocumentPart(content="")) if tq_verses: document_parts.append( DocumentPart( @@ -256,12 +294,23 @@ def assemble_usfm_by_book( is_rtl=is_rtl, add_hr_p=False, contained_in_two_column_section=True, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=use_section_visual_separator, ) ) - document_parts.append(DocumentPart(content="")) if rg_verses: document_parts.append( - DocumentPart(content=rg_verses, is_rtl=is_rtl, add_hr_p=False) + DocumentPart( + content=rg_verses, + is_rtl=is_rtl, + add_hr_p=False, + use_section_visual_separator=use_section_visual_separator, + ) ) # TODO Get feedback on whether we should allow a user to select a primary _and_ # a secondary USFM resource. If we want to limit the user to only one USFM per @@ -276,6 +325,7 @@ def assemble_usfm_by_book( is_rtl=usfm_book2 and usfm_book2.lang_direction == LangDirEnum.RTL, contained_in_two_column_section=True, + use_section_visual_separator=use_section_visual_separator, ) ) document_parts.append( @@ -283,6 +333,7 @@ def assemble_usfm_by_book( content="", add_hr_p=False, add_page_break=True, + use_section_visual_separator=use_section_visual_separator, ) ) return document_parts @@ -296,6 +347,7 @@ def assemble_tn_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], + use_section_visual_separator: bool, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, ) -> list[DocumentPart]: """ @@ -309,6 +361,7 @@ def assemble_tn_by_book( DocumentPart( content=tn_book.book_intro, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) if bc_book and bc_book.book_intro: @@ -316,20 +369,30 @@ def assemble_tn_by_book( for chapter_num in tn_book.chapters: one_column_html = [] one_column_html.append(chapter_heading(chapter_num)) - one_column_html.append(chapter_intro(tn_book, chapter_num)) + one_column_html.append( + chapter_intro(tn_book, chapter_num, use_section_visual_separator) + ) one_column_html_ = "".join(one_column_html) if one_column_html_: document_parts.append( DocumentPart( content=one_column_html_, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) if bc_book: document_parts.append( - DocumentPart(content=chapter_commentary(bc_book, chapter_num)) + DocumentPart( + content=chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ), + use_section_visual_separator=use_section_visual_separator, + ) ) - tn_verses = tn_chapter_verses(tn_book, chapter_num) + tn_verses = tn_chapter_verses( + tn_book, chapter_num, use_section_visual_separator + ) if tn_verses: document_parts.append( DocumentPart( @@ -337,31 +400,47 @@ def assemble_tn_by_book( is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, add_hr_p=False, contained_in_two_column_section=True, + use_section_visual_separator=use_section_visual_separator, ) ) document_parts.append(DocumentPart(content="")) - tq_verses = tq_chapter_verses(tq_book, chapter_num) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) if tq_book and tq_verses: document_parts.append( DocumentPart( content=tq_verses, is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, contained_in_two_column_section=True, + use_section_visual_separator=use_section_visual_separator, ) ) document_parts.append(DocumentPart(content="")) - rg_verses = rg_chapter_verses(rg_book, chapter_num) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) if rg_book and rg_verses: - # add_two_column_section(doc) document_parts.append( DocumentPart( content=rg_verses, is_rtl=rg_book and rg_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=use_section_visual_separator, ) ) - document_parts.append(DocumentPart(content="")) document_parts.append( - DocumentPart(content="", add_hr_p=False, add_page_break=True) + DocumentPart( + content="", + add_hr_p=False, + add_page_break=True, + use_section_visual_separator=use_section_visual_separator, + ) ) return document_parts @@ -374,6 +453,7 @@ def assemble_tq_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], + use_section_visual_separator: bool, ) -> list[DocumentPart]: """ Construct the HTML for a 'by book' strategy wherein at least @@ -384,33 +464,50 @@ def assemble_tq_by_book( for chapter_num in tq_book.chapters: if bc_book: document_parts.append( - DocumentPart(content=chapter_commentary(bc_book, chapter_num)) + DocumentPart( + content=chapter_commentary( + bc_book, chapter_num, use_section_visual_separator + ), + use_section_visual_separator=use_section_visual_separator, + ) ) document_parts.append( DocumentPart( content=chapter_heading(chapter_num), is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) - tq_verses = tq_chapter_verses(tq_book, chapter_num) + tq_verses = tq_chapter_verses( + tq_book, chapter_num, use_section_visual_separator + ) if tq_verses: document_parts.append( DocumentPart( content=tq_verses, is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, contained_in_two_column_section=True, + use_section_visual_separator=use_section_visual_separator, ) ) - rg_verses = rg_chapter_verses(rg_book, chapter_num) + rg_verses = rg_chapter_verses( + rg_book, chapter_num, use_section_visual_separator + ) if rg_book and rg_verses: document_parts.append( DocumentPart( content=rg_verses, is_rtl=rg_book and rg_book.lang_direction == LangDirEnum.RTL, + use_section_visual_separator=use_section_visual_separator, ) ) document_parts.append( - DocumentPart(content="", add_hr_p=False, add_page_break=True) + DocumentPart( + content="", + add_hr_p=False, + add_page_break=True, + use_section_visual_separator=use_section_visual_separator, + ) ) return document_parts @@ -423,6 +520,7 @@ def assemble_tw_by_book( usfm_book2: Optional[USFMBook], bc_book: Optional[BCBook], rg_book: Optional[RGBook], + use_section_visual_separator: bool, ) -> list[DocumentPart]: """ TW is handled outside this module, that is why no @@ -432,8 +530,18 @@ def assemble_tw_by_book( if bc_book: document_parts.append(DocumentPart(content=bc_book.book_intro)) for chapter in bc_book.chapters.values(): - document_parts.append(DocumentPart(content=chapter.commentary)) document_parts.append( - DocumentPart(content="", add_hr_p=False, add_page_break=True) + DocumentPart( + content=chapter.commentary, + use_section_visual_separator=use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + add_hr_p=False, + add_page_break=True, + use_section_visual_separator=use_section_visual_separator, + ) ) return document_parts diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index fedf3103..b21ed67b 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -105,6 +105,7 @@ def generate_document( document_request.chunk_size, document_request.limit_words, document_request.use_chapter_labels, + document_request.use_section_visual_separator, ) html_filepath_ = html_filepath(document_request_key_) pdf_filepath_ = pdf_filepath(document_request_key_) @@ -237,6 +238,7 @@ def generate_docx_document( document_request.chunk_size, document_request.limit_words, document_request.use_chapter_labels, + document_request.use_section_visual_separator, ) html_filepath_ = html_filepath(document_request_key_) docx_filepath_ = docx_filepath(document_request_key_) @@ -313,6 +315,7 @@ def generate_docx_document( docx_filepath_, document_parts, document_request.layout_for_print, + document_request.use_section_visual_separator, title1, title2, ) @@ -344,6 +347,7 @@ def document_request_key( chunk_size: ChunkSizeEnum, limit_words: bool, use_chapter_labels: bool, + use_section_visual_separator: bool, max_filename_len: int = 240, underscore: str = "_", hyphen: str = "-", @@ -376,9 +380,9 @@ def document_request_key( ] ) if any(contains_tw(resource_request) for resource_request in resource_requests): - document_request_key = f'{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}_{"lwt" if limit_words else "lwf"}' + document_request_key = f'{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}_{"lwt" if limit_words else "lwf"}_{"sst" if use_section_visual_separator else "ssf"}' else: - document_request_key = f"{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}" + document_request_key = f'{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}_{"sst" if use_section_visual_separator else "ssf"}' if len(document_request_key) >= max_filename_len: # The generated filename could be too long for the OS where this is # running. Therefore, use the current time as a document_request_key @@ -451,18 +455,19 @@ def assemble_content( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], found_resource_lookup_dtos: Sequence[ResourceLookupDto], + hr: str = "
", ) -> str: """ Assemble and return the content from all requested resources according to the assembly_strategy requested. """ t0 = time.time() - content = "" + content = [] if ( document_request.assembly_strategy_kind == AssemblyStrategyEnum.LANGUAGE_BOOK_ORDER ): - content = "".join( + content.append( assemble_content_by_lang_then_book( usfm_books, tn_books, @@ -471,13 +476,14 @@ def assemble_content( bc_books, rg_books, cast(AssemblyLayoutEnum, document_request.assembly_layout_kind), + document_request.use_section_visual_separator, ) ) elif ( document_request.assembly_strategy_kind == AssemblyStrategyEnum.BOOK_LANGUAGE_ORDER ): - content = "".join( + content.append( assemble_content_by_book_then_lang( usfm_books, tn_books, @@ -486,6 +492,7 @@ def assemble_content( bc_books, rg_books, cast(AssemblyLayoutEnum, document_request.assembly_layout_kind), + document_request.use_section_visual_separator, ) ) t1 = time.time() @@ -496,10 +503,19 @@ def assemble_content( for tw_book in tw_books: if tw_book.lang_code not in unique_lang_codes: unique_lang_codes.add(tw_book.lang_code) - content = f"{content}{translation_words_section(tw_book, usfm_books, document_request.limit_words, document_request.resource_requests)}
" + content.append( + translation_words_section( + tw_book, + usfm_books, + document_request.limit_words, + document_request.resource_requests, + ) + ) + if document_request.use_section_visual_separator: + content.append(hr) t1 = time.time() logger.info("Time for add TW content to document: %s", t1 - t0) - return content + return "".join(content) def create_title_page_and_wrap_in_template( @@ -553,6 +569,7 @@ def assemble_docx_content( rg_books, cast(AssemblyLayoutEnum, document_request.assembly_layout_kind), document_request.chunk_size, + document_request.use_section_visual_separator, ) elif ( document_request.assembly_strategy_kind @@ -567,6 +584,7 @@ def assemble_docx_content( rg_books, cast(AssemblyLayoutEnum, document_request.assembly_layout_kind), document_request.chunk_size, + document_request.use_section_visual_separator, ) t1 = time.time() logger.info("Time for interleaving document: %s", t1 - t0) @@ -582,10 +600,16 @@ def assemble_docx_content( usfm_books, document_request.limit_words, document_request.resource_requests, - ) + ), + use_section_visual_separator=document_request.use_section_visual_separator, + ) + ) + document_parts.append( + DocumentPart( + content="", + use_section_visual_separator=document_request.use_section_visual_separator, ) ) - document_parts.append(DocumentPart(content="")) t1 = time.time() logger.info("Time for adding TW content to document: %s", t1 - t0) return document_parts @@ -657,7 +681,9 @@ def convert_html_to_epub( logger.info("Time for converting HTML to ePub: %s", t1 - t0) -def compose_document(document_parts: list[DocumentPart]) -> Document: +def compose_document( + document_parts: list[DocumentPart], use_section_visual_separator: bool +) -> Document: doc = Document() html_to_docx = HtmlToDocx() for part in document_parts: @@ -669,7 +695,7 @@ def compose_document(document_parts: list[DocumentPart]) -> Document: html_to_docx.add_html_to_document(part.content, doc) # Set the language for spellcheck # set_docx_language(doc, lang_code) - if part.add_hr_p: + if use_section_visual_separator and part.add_hr_p: add_hr(doc.paragraphs[-1]) if part.add_page_break: add_page_break(doc) @@ -681,6 +707,7 @@ def convert_html_to_docx( docx_filepath: str, document_parts: list[DocumentPart], layout_for_print: bool, + use_section_visual_separator: bool, title1: str = "title1", title2: str = "title2", title3: str = "", @@ -710,7 +737,7 @@ def convert_html_to_docx( new_section = doc.add_section(WD_SECTION.CONTINUOUS) new_section.start_type master = Composer(doc) - master.append(compose_document(document_parts)) + master.append(compose_document(document_parts, use_section_visual_separator)) master.save(docx_filepath) t1 = time.time() logger.info("Time for converting HTML to Docx: %s", t1 - t0) diff --git a/backend/doc/domain/model.py b/backend/doc/domain/model.py index 0a431357..ef6c644c 100644 --- a/backend/doc/domain/model.py +++ b/backend/doc/domain/model.py @@ -201,6 +201,8 @@ class DocumentRequest(BaseModel): # is True, then the chapter label will be localized to the language(s) # requested. use_chapter_labels: bool = False + # Indiciate whether to show visual separator between sections, e.g., hr element + use_section_visual_separator: bool = False # Indicate whether TN book intros should be included. Currently, # the content team does not want them included. include_tn_book_intros: bool = False @@ -554,3 +556,4 @@ class DocumentPart(BaseModel): add_hr_p: bool = True contained_in_two_column_section: bool = False add_page_break: bool = False + use_section_visual_separator: bool = False diff --git a/frontend/src/lib/stores/SettingsStore.ts b/frontend/src/lib/stores/SettingsStore.ts index 49c5596b..e8a02784 100644 --- a/frontend/src/lib/stores/SettingsStore.ts +++ b/frontend/src/lib/stores/SettingsStore.ts @@ -16,3 +16,4 @@ export let emailStore: Writable = writable(null) export let documentRequestKeyStore: Writable = writable('') export let settingsUpdated: Writable = writable(false) export let useChapterLabelsStore: Writable = writable(false) +export let useSectionVisualSeparatorStore: Writable = writable(false) diff --git a/frontend/src/routes/books/+page.svelte b/frontend/src/routes/books/+page.svelte index 03dc6802..f6df0da5 100644 --- a/frontend/src/routes/books/+page.svelte +++ b/frontend/src/routes/books/+page.svelte @@ -40,9 +40,10 @@ async function loadBookCodesAndNames() { try { - const bookCodesAndNames = $langCountStore > 1 - ? await getBookCodesAndNames($langCodesStore[0], $langCodesStore[1]) - : await getBookCodesAndNames($langCodesStore[0]) + const bookCodesAndNames = + $langCountStore > 1 + ? await getBookCodesAndNames($langCodesStore[0], $langCodesStore[1]) + : await getBookCodesAndNames($langCodesStore[0]) updateStores(bookCodesAndNames) } catch (err) { console.error(err) @@ -59,13 +60,13 @@ .filter(([code]) => otBooks.includes(code)) .map(([code, name]) => `${code}, ${name}`) if ($otBookStore.length > 0) { - $otBookStore = $otBookStore.filter(item => otBookCodes.includes(item)) + $otBookStore = $otBookStore.filter((item) => otBookCodes.includes(item)) } ntBookCodes = bookCodesAndNames .filter(([code]) => !otBooks.includes(code)) .map(([code, name]) => `${code}, ${name}`) if ($ntBookStore.length > 0) { - $ntBookStore = $ntBookStore.filter(item => ntBookCodes.includes(item)) + $ntBookStore = $ntBookStore.filter((item) => ntBookCodes.includes(item)) } } @@ -238,7 +239,7 @@ +
+ + Show visual separator (horizontal line) between sections +
{/if}

Notification

diff --git a/frontend/src/routes/settings/GenerateDocument.svelte b/frontend/src/routes/settings/GenerateDocument.svelte index c7a95ad5..27a8117e 100644 --- a/frontend/src/routes/settings/GenerateDocument.svelte +++ b/frontend/src/routes/settings/GenerateDocument.svelte @@ -19,9 +19,10 @@ emailStore, documentRequestKeyStore, settingsUpdated, - useChapterLabelsStore + useChapterLabelsStore, + useSectionVisualSeparatorStore } from '$lib/stores/SettingsStore' - import { taskIdStore, taskStateStore } from '$lib/stores/TaskStore' + import { taskStateStore } from '$lib/stores/TaskStore' import { getCode, getResourceTypeLangCode, getResourceTypeCode } from '$lib/utils' import LogRocket from 'logrocket' import TaskStatus from './TaskStatus.svelte' @@ -86,7 +87,8 @@ resource_requests: resourceRequests, document_request_source: 'ui', limit_words: $limitTwStore, - use_chapter_labels: $useChapterLabelsStore + use_chapter_labels: $useChapterLabelsStore, + use_section_visual_separator: $useSectionVisualSeparatorStore } console.log('document request: ', JSON.stringify(documentRequest, null, 2)) $errorStore = null diff --git a/tests/unit/test_document_generator.py b/tests/unit/test_document_generator.py index a3629ca6..2f5b455f 100644 --- a/tests/unit/test_document_generator.py +++ b/tests/unit/test_document_generator.py @@ -84,6 +84,7 @@ def test_document_request_key_too_long_for_semantic_result() -> None: chunk_size = model.ChunkSizeEnum.CHAPTER limit_words = True use_chapter_labels = True + use_section_visual_separator = True key = document_generator.document_request_key( resource_requests, assembly_strategy_kind, @@ -91,5 +92,6 @@ def test_document_request_key_too_long_for_semantic_result() -> None: chunk_size, limit_words, use_chapter_labels, + use_section_visual_separator, ) assert re.search(r"[0-9]+_[0-9]+", key) From 5086b7c7addb826b057ba7d13aa1ea860deffc97 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 15 Jul 2025 11:41:09 -0700 Subject: [PATCH 111/208] Add tests for use section visual separator option and book ordering --- frontend/tests/e2e/test.ts | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/frontend/tests/e2e/test.ts b/frontend/tests/e2e/test.ts index d5addfa1..e44893c3 100644 --- a/frontend/tests/e2e/test.ts +++ b/frontend/tests/e2e/test.ts @@ -206,4 +206,88 @@ test('test limit tw words switch', async ({ page }) => { await page.getByLabel('Translation Words tw').check() await page.getByRole('button', { name: 'Next' }).click() await expect(page.getByRole('main')).toContainText('Limit TW words') +test('test use section visual separator setting', async ({ page }) => { + await page.goto('http://localhost:8001/') + await page.getByText('Español Latin America (Latin').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Efesios').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Select all').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() + await page.locator('div').filter({ hasText: 'Show visual separator (' }).nth(4).click() + await page.getByRole('button', { name: 'Generate File' }).click() +}) + +test('test ordering of books in document title(s) and body', async ({ page }) => { + await page.goto('http://localhost:8001/') + await page.getByPlaceholder('Search Gateway Languages').click() + await page.getByPlaceholder('Search Gateway Languages').fill('tpi') + await page.getByText('Tok Pisin').click() + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByPlaceholder('Search Heart Languages').click() + await page.getByPlaceholder('Search Heart Languages').fill('ont') + await page.getByText('Ontenu').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Matyu').click() + await page.getByText('Mak').click() + await page.getByText('Luk', { exact: true }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Unlocked Literal Bible').click() + await page.getByText('Regular').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('PDF').click() + await page.getByText('Interleave content by chapter').click() + await page.getByRole('button', { name: 'Generate File' }).click() + // Wait for the "View HTML Online" button to appear and become visible + const viewHtmlButton = page.getByRole('button', { name: 'View HTML Online' }) + await viewHtmlButton.waitFor({ state: 'visible' }) + // Start waiting for the popup BEFORE clicking + const page1Promise = page.waitForEvent('popup') + // Click to trigger the popup + await viewHtmlButton.click() + // Get the new popup page + const page1 = await page1Promise + // Perform text expectations on the popup page + await expect(page1.locator('body')).toContainText( + 'Ontenu (Ontenu): Regular for Matthew, Maki, Luk' + ) + await expect(page1.locator('body')).toContainText( + 'Tok Pisin (Tok Pisin): Unlocked Literal Bible for Matyu, Mak, Luk' + ) + + // Order assertions + const headings = await page1.locator('h2').allTextContents() + // Ensure expected headings are present + expect(headings).toContain('Matthew') + expect(headings).toContain('Matyu') + expect(headings).toContain('Maki') + expect(headings).toContain('Mak') + expect(headings).toContain('Luk') + + // Find the positions of each + const index1 = headings.indexOf('Matthew') + const index2 = headings.indexOf('Matyu') + const index3 = headings.indexOf('Maki') + const index4 = headings.indexOf('Mak') + const index5 = headings.indexOf('Luk') + + // Ensure all were found + expect(index1).not.toBe(-1) + expect(index2).not.toBe(-1) + expect(index3).not.toBe(-1) + expect(index4).not.toBe(-1) + expect(index5).not.toBe(-1) + + // Check the order + expect(index1).toBeLessThan(index2) + expect(index1).toBeLessThan(index3) + expect(index2).toBeLessThan(index3) + expect(index1).toBeLessThan(index4) + expect(index2).toBeLessThan(index4) + expect(index3).toBeLessThan(index4) + expect(index1).toBeLessThan(index5) + expect(index2).toBeLessThan(index5) + expect(index3).toBeLessThan(index5) + expect(index4).toBeLessThan(index5) }) From 926ec328c880ad3fe78196b0984a1d103ba628ad Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 15 Jul 2025 11:41:44 -0700 Subject: [PATCH 112/208] Automatic code formatting (prettier) --- frontend/tests/e2e/test.ts | 363 ++++++++++++++++++++----------------- 1 file changed, 192 insertions(+), 171 deletions(-) diff --git a/frontend/tests/e2e/test.ts b/frontend/tests/e2e/test.ts index e44893c3..d0e057f2 100644 --- a/frontend/tests/e2e/test.ts +++ b/frontend/tests/e2e/test.ts @@ -1,211 +1,232 @@ import { expect, test } from '@playwright/test' test('test ui part 1', async ({ page }) => { - await page.goto('http://localhost:8001/') - await page.getByText('Tiếng Việt (Vietnamese)').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Ga-la-ti gal').click() - await page.getByText('LU-CA luk').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Unlocked Literal Bible').click() - await page.getByRole('button', { name: 'Edit' }).first().click() - await page.getByRole('button', { name: 'Heart' }).click() - await page.getByRole('button', { name: 'Gateway' }).click() - await page.getByText('অসমীয়া (Assamese) as').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Unlocked Literal Bible').nth(1).click({timeout: 320000}) - await page.getByText('Translation Notes').first().click() - await page.getByText('Translation Notes').nth(1).click() - await page.getByText('Translation Questions').first().click() - await page.getByText('Translation Questions').nth(1).click() - await page.getByText('Translation Words').first().click() - await page.getByText('Translation Words').nth(1).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Generate File' }).click() + await page.goto('http://localhost:8001/') + await page.getByText('Tiếng Việt (Vietnamese)').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Ga-la-ti gal').click() + await page.getByText('LU-CA luk').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Unlocked Literal Bible').click() + await page.getByRole('button', { name: 'Edit' }).first().click() + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByRole('button', { name: 'Gateway' }).click() + await page.getByText('অসমীয়া (Assamese) as').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Unlocked Literal Bible').nth(1).click({ timeout: 320000 }) + await page.getByText('Translation Notes').first().click() + await page.getByText('Translation Notes').nth(1).click() + await page.getByText('Translation Questions').first().click() + await page.getByText('Translation Questions').nth(1).click() + await page.getByText('Translation Words').first().click() + await page.getByText('Translation Words').nth(1).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Generate File' }).click() }) test('test ui part 2', async ({ page }) => { - await page.goto('http://localhost:8001') - await page.getByText('English').click() - await page.getByText('Español Latin America (Latin').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Galatians').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText(/.*Unlocked Literal Bible.*/).first().click() - await page.getByText(/.*Unlocked Literal Bible.*/).nth(1).click() - await page.getByText(/.*Translation Notes.*/).nth(1).click({timeout: 8200000}) - await page .getByText(/.*Translation Notes.*/).nth(2).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('PDF').click() - await page.getByText('Interleave content by chapter').click() - await page.getByRole('button', { name: 'Generate File' }).click() + await page.goto('http://localhost:8001') + await page.getByText('English').click() + await page.getByText('Español Latin America (Latin').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Galatians').click() + await page.getByRole('button', { name: 'Next' }).click() + await page + .getByText(/.*Unlocked Literal Bible.*/) + .first() + .click() + await page + .getByText(/.*Unlocked Literal Bible.*/) + .nth(1) + .click() + await page + .getByText(/.*Translation Notes.*/) + .nth(1) + .click({ timeout: 8200000 }) + await page + .getByText(/.*Translation Notes.*/) + .nth(2) + .click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('PDF').click() + await page.getByText('Interleave content by chapter').click() + await page.getByRole('button', { name: 'Generate File' }).click() }) test('test books retained in basket on back button to languages and then forward', async ({ - page + page }) => { - await page.goto('http://localhost:8001/') - await page.getByPlaceholder('Search Gateway Languages').click() - await page.getByPlaceholder('Search Gateway Languages').fill('Amh') - await page.getByText('አማርኛ (Amharic)').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('2 ኛ ቆሮንቶስ').click({ timeout: 680000}) - await page.getByRole('link', { name: 'Languages' }).click() - await page.getByRole('button', { name: 'Heart' }).click() - await page.getByPlaceholder('Search Heart Languages').click() - await page.getByPlaceholder('Search Heart Languages').fill('adh') - await page.getByText('Adhola adh').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByPlaceholder('Search NT books').click() - await page.getByPlaceholder('Search NT books').fill('2 ኛ ዮሐንስ') - await page.getByText('2 ኛ ዮሐንስ').click({timeout: 5800000}) - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.locator('body')).toContainText('Adhola') - await page.getByText('Unlocked Literal Bible').click() - await page.getByText('Regular', { exact: true }).click() - await page.getByRole('link', { name: 'Languages' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByPlaceholder('Search NT books').click() - await page.getByPlaceholder('Search NT books').fill('2 ኛ ዮሐንስ') + await page.goto('http://localhost:8001/') + await page.getByPlaceholder('Search Gateway Languages').click() + await page.getByPlaceholder('Search Gateway Languages').fill('Amh') + await page.getByText('አማርኛ (Amharic)').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('2 ኛ ቆሮንቶስ').click({ timeout: 680000 }) + await page.getByRole('link', { name: 'Languages' }).click() + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByPlaceholder('Search Heart Languages').click() + await page.getByPlaceholder('Search Heart Languages').fill('adh') + await page.getByText('Adhola adh').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByPlaceholder('Search NT books').click() + await page.getByPlaceholder('Search NT books').fill('2 ኛ ዮሐንስ') + await page.getByText('2 ኛ ዮሐንስ').click({ timeout: 5800000 }) + await page.getByRole('button', { name: 'Next' }).click() + await expect(page.locator('body')).toContainText('Adhola') + await page.getByText('Unlocked Literal Bible').click() + await page.getByText('Regular', { exact: true }).click() + await page.getByRole('link', { name: 'Languages' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByPlaceholder('Search NT books').click() + await page.getByPlaceholder('Search NT books').fill('2 ኛ ዮሐንስ') }) - test('test transfer from biel', async ({ page }) => { - await page.goto( - 'http://localhost:8001/transfer/repo_url=https%3A%2F%2Fcontent.bibletranslationtools.org%2Fchunga_moses%2Fleb-x-bisa_col_text_reg&book_name=Colossians' - ) - await expect(page.getByText('Bisa')).toBeVisible() - await expect(page.getByText('Colossians')).toBeVisible({ timeout: 1200000 }) + await page.goto( + 'http://localhost:8001/transfer/repo_url=https%3A%2F%2Fcontent.bibletranslationtools.org%2Fchunga_moses%2Fleb-x-bisa_col_text_reg&book_name=Colossians' + ) + await expect(page.getByText('Bisa')).toBeVisible() + await expect(page.getByText('Colossians')).toBeVisible({ timeout: 1200000 }) }) test('test transfer from biel 2', async ({ page }) => { - await page.goto( - 'http://localhost:8001/transfer/repo_url=https:%2F%2Fcontent.bibletranslationtools.org%2FWycliffeAssociates%2Fen_ulb' - ) - await expect(page.getByText('English')).toBeVisible({ timeout: 20000 }) - await expect(page.getByText('Genesis')).toBeVisible() - await expect(page.getByText('Deuteronomy')).toBeVisible() - await expect(page.getByText('(60) items hidden')).toBeVisible() + await page.goto( + 'http://localhost:8001/transfer/repo_url=https:%2F%2Fcontent.bibletranslationtools.org%2FWycliffeAssociates%2Fen_ulb' + ) + await expect(page.getByText('English')).toBeVisible({ timeout: 20000 }) + await expect(page.getByText('Genesis')).toBeVisible() + await expect(page.getByText('Deuteronomy')).toBeVisible() + await expect(page.getByText('(60) items hidden')).toBeVisible() }) test('test es-419 resource types', async ({ page }) => { - await page.goto('http://localhost:8001') - await page.getByText(/.*Español.*/).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Mateo').click({timeout: 60000}) - await page.getByRole('button', { name: 'Next' }).click() - await page.getByLabel('Translation Notes').check() - await page.getByLabel('Translation Questions').check() - await page.locator('span').filter({ hasText: 'Español Latin America (Latin American Spanish)' }) - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Generate File' }).click() + await page.goto('http://localhost:8001') + await page.getByText(/.*Español.*/).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Mateo').click({ timeout: 60000 }) + await page.getByRole('button', { name: 'Next' }).click() + await page.getByLabel('Translation Notes').check() + await page.getByLabel('Translation Questions').check() + await page.locator('span').filter({ hasText: 'Español Latin America (Latin American Spanish)' }) + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Generate File' }).click() }) - -test('test that reviewers guide is only shown when book is chosen that it includes', async ({ page }) => { - await page.goto('http://localhost:8001/languages') - await page.getByText('English').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('1 Corinthians').click() - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.locator('li').filter({ hasText: "NT Survey Reviewers' Guide" })).not.toBeVisible({timeout: 580000}) +test('test that reviewers guide is only shown when book is chosen that it includes', async ({ + page +}) => { + await page.goto('http://localhost:8001/languages') + await page.getByText('English').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('1 Corinthians').click() + await page.getByRole('button', { name: 'Next' }).click() + await expect( + page.locator('li').filter({ hasText: "NT Survey Reviewers' Guide" }) + ).not.toBeVisible({ timeout: 580000 }) }) -test('test that reviewers guide is only shown when book is chosen that it includes - part 2', async ({ page }) => { - await page.goto('http://localhost:8001/languages') - await page.getByText('English').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Galatians').click({ timeout: 60000 }) - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.locator('span').filter({ hasText: "NT Survey Reviewers' Guide" })).toBeVisible({ timeout: 5800000 }) +test('test that reviewers guide is only shown when book is chosen that it includes - part 2', async ({ + page +}) => { + await page.goto('http://localhost:8001/languages') + await page.getByText('English').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Galatians').click({ timeout: 60000 }) + await page.getByRole('button', { name: 'Next' }).click() + await expect( + page.locator('span').filter({ hasText: "NT Survey Reviewers' Guide" }) + ).toBeVisible({ timeout: 5800000 }) }) - -test('test that you can select gateway tab after first selecting heart language and hitting next', async ({ page }) => { - await page.goto('http://localhost:8001/') - await page.getByRole('button', { name: 'Heart' }).click() - await page.getByText('Adhola').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('1 Thesalonika').click({ timeout: 580000 }) - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('link', { name: 'Languages' }).click() - await page.getByRole('button', { name: 'Gateway' }).click() - await page.getByText('Cebuano').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Mga taga tesalonica 1th').click({ timeout: 580000 }) - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Unlocked Literal Bible').click() - await page.getByText('Regular', { exact: true }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('PDF').click() - await page.getByText('Interleave content by chapter').click() - await page.getByRole('button', { name: 'Generate File' }).click() +test('test that you can select gateway tab after first selecting heart language and hitting next', async ({ + page +}) => { + await page.goto('http://localhost:8001/') + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByText('Adhola').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('1 Thesalonika').click({ timeout: 580000 }) + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('link', { name: 'Languages' }).click() + await page.getByRole('button', { name: 'Gateway' }).click() + await page.getByText('Cebuano').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Mga taga tesalonica 1th').click({ timeout: 580000 }) + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Unlocked Literal Bible').click() + await page.getByText('Regular', { exact: true }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('PDF').click() + await page.getByText('Interleave content by chapter').click() + await page.getByRole('button', { name: 'Generate File' }).click() }) - test('test optional settings', async ({ page }) => { - await page.goto('http://localhost:8001/languages') - await page.getByText('Bichelamar (Bislama)').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Matiu').click({ timeout: 120000 }) - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Regular').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('PDF').click() - await expect(page.getByRole('main')).toContainText('▶ Show Optional Settings') - await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() - await expect(page.getByRole('main')).toContainText('Use chapter labels, e.g., \'Chapter 1\' instead of \'1\'') + await page.goto('http://localhost:8001/languages') + await page.getByText('Bichelamar (Bislama)').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Matiu').click({ timeout: 120000 }) + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Regular').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('PDF').click() + await expect(page.getByRole('main')).toContainText('▶ Show Optional Settings') + await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() + await expect(page.getByRole('main')).toContainText( + "Use chapter labels, e.g., 'Chapter 1' instead of '1'" + ) }) - test.skip('test aba philemon', async ({ page }) => { - await page.goto('http://localhost:8001/') - await page.getByRole('button', { name: 'Heart' }).click() - await page.getByText('Abé aba').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Philémon').click({timeout: 580000}) - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Regular').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('PDF').click() - await page.getByRole('button', { name: 'Generate File' }).click() - await expect(page.locator('body')).toContainText('Philémon') - await expect(page.locator('body')).toContainText('Regular (aba)') + await page.goto('http://localhost:8001/') + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByText('Abé aba').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Philémon').click({ timeout: 580000 }) + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Regular').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('PDF').click() + await page.getByRole('button', { name: 'Generate File' }).click() + await expect(page.locator('body')).toContainText('Philémon') + await expect(page.locator('body')).toContainText('Regular (aba)') }) test('test that book name correction happened for pt-br, 1co', async ({ page }) => { - await page.goto('http://localhost:8001/') - await page.getByText('Português Brasileiro (').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByLabel('Coríntios 1co').check() - await expect(page.locator('body')).toContainText('1 Coríntios') + await page.goto('http://localhost:8001/') + await page.getByText('Português Brasileiro (').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByLabel('Coríntios 1co').check() + await expect(page.locator('body')).toContainText('1 Coríntios') }) test('test limit tw words switch', async ({ page }) => { - await page.goto('http://localhost:8001/') - await page.getByText('Cebuano').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Mateo').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Unlocked Literal Bible ulb').click() - await page.getByText('Translation Words').click() - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.getByRole('main')).toContainText('Limit TW words') - await page.getByRole('link', { name: 'Resources' }).click() - await page.getByLabel('Unlocked Literal Bible ulb').uncheck() - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.getByRole('main')).not.toContainText('Limit TW words') - await page.getByRole('link', { name: 'Resources' }).click() - await page.getByLabel('Unlocked Literal Bible ulb').check() - await page.getByLabel('Translation Words tw').uncheck() - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.getByRole('main')).not.toContainText('Limit TW words') - await page.getByRole('link', { name: 'Resources' }).click() - await page.getByLabel('Unlocked Literal Bible ulb').check() - await page.getByLabel('Translation Words tw').check() - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.getByRole('main')).toContainText('Limit TW words') + await page.goto('http://localhost:8001/') + await page.getByText('Cebuano').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Mateo').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Unlocked Literal Bible ulb').click() + await page.getByText('Translation Words').click() + await page.getByRole('button', { name: 'Next' }).click() + await expect(page.getByRole('main')).toContainText('Limit TW words') + await page.getByRole('link', { name: 'Resources' }).click() + await page.getByLabel('Unlocked Literal Bible ulb').uncheck() + await page.getByRole('button', { name: 'Next' }).click() + await expect(page.getByRole('main')).not.toContainText('Limit TW words') + await page.getByRole('link', { name: 'Resources' }).click() + await page.getByLabel('Unlocked Literal Bible ulb').check() + await page.getByLabel('Translation Words tw').uncheck() + await page.getByRole('button', { name: 'Next' }).click() + await expect(page.getByRole('main')).not.toContainText('Limit TW words') + await page.getByRole('link', { name: 'Resources' }).click() + await page.getByLabel('Unlocked Literal Bible ulb').check() + await page.getByLabel('Translation Words tw').check() + await page.getByRole('button', { name: 'Next' }).click() + await expect(page.getByRole('main')).toContainText('Limit TW words') +}) + test('test use section visual separator setting', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByText('Español Latin America (Latin').click() From 58be37b71813d66fc57f7cc11ae0acea64aecfbf Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 15 Jul 2025 12:11:59 -0700 Subject: [PATCH 113/208] Remove use of Any type --- backend/doc/domain/document_generator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index b21ed67b..f101aa82 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -7,7 +7,7 @@ import time from datetime import datetime from os.path import exists, join -from typing import Any, Optional, Sequence, cast +from typing import Optional, Sequence, cast from celery import current_task from doc.config import settings @@ -82,7 +82,8 @@ # ) @worker.app.task def generate_document( - document_request_json: str, output_dir: str = settings.DOCUMENT_OUTPUT_DIR + document_request_json: str, + output_dir: str = settings.DOCUMENT_OUTPUT_DIR, ) -> str: """ This is the main entry point for this module for non-docx generation. @@ -215,7 +216,7 @@ def generate_document( @worker.app.task def generate_docx_document( - document_request_json: Json[Any], + document_request_json: str, output_dir: str = settings.DOCUMENT_OUTPUT_DIR, ) -> Json[str]: """ From 7683e77d2640d47546a56d6188ca2195f1dcb7df Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 15 Jul 2025 12:13:30 -0700 Subject: [PATCH 114/208] Remove use of Json type --- backend/doc/domain/document_generator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index f101aa82..75a003ca 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -70,7 +70,6 @@ from docxcompose.composer import Composer # type: ignore from docxtpl import DocxTemplate # type: ignore from htmldocx import HtmlToDocx # type: ignore -from pydantic import Json logger = settings.logger(__name__) @@ -218,7 +217,7 @@ def generate_document( def generate_docx_document( document_request_json: str, output_dir: str = settings.DOCUMENT_OUTPUT_DIR, -) -> Json[str]: +) -> str: """ This is the alternative entry point for Docx document creation only. """ From 0c734240a0eec26033f4254c38e47f2aa5b4d726 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 15 Jul 2025 16:17:42 -0700 Subject: [PATCH 115/208] Code deduplication in service of easier unit testing Use analogous document parts being interpreted to a whole as we do for docx generation. A nice side effect is that we get easier testing of content, e.g., ordering, as well. --- ...ly_strategies_book_then_lang_by_chapter.py | 30 +- ...ly_strategies_lang_then_book_by_chapter.py | 32 +-- backend/doc/domain/document_generator.py | 259 +++++++++--------- 3 files changed, 156 insertions(+), 165 deletions(-) diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py index d6128c6d..0aa391a6 100644 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py @@ -96,7 +96,7 @@ def assemble_content_by_book_then_lang( assembly_layout_kind == AssemblyLayoutEnum.ONE_COLUMN or assembly_layout_kind == AssemblyLayoutEnum.ONE_COLUMN_COMPACT ): - content.append( + content.extend( assemble_usfm_by_chapter( selected_usfm_books, selected_tn_books, @@ -115,7 +115,7 @@ def assemble_content_by_book_then_lang( or assembly_layout_kind == AssemblyLayoutEnum.ONE_COLUMN_COMPACT ) ): - content.append( + content.extend( assemble_tn_by_chapter( selected_usfm_books, selected_tn_books, @@ -135,7 +135,7 @@ def assemble_content_by_book_then_lang( or assembly_layout_kind == AssemblyLayoutEnum.ONE_COLUMN_COMPACT ) ): - content.append( + content.extend( assemble_tq_by_chapter( selected_usfm_books, selected_tn_books, @@ -156,7 +156,7 @@ def assemble_content_by_book_then_lang( or assembly_layout_kind == AssemblyLayoutEnum.ONE_COLUMN_COMPACT ) ): - content.append( + content.extend( assemble_tw_by_chapter( selected_usfm_books, selected_tn_books, @@ -173,7 +173,7 @@ def assemble_content_by_book_then_lang( or assembly_layout_kind == AssemblyLayoutEnum.TWO_COLUMN_SCRIPTURE_LEFT_SCRIPTURE_RIGHT_COMPACT ): - content.append( + content.extend( assemble_usfm_by_chapter_2c_sl_sr( selected_usfm_books, selected_tn_books, @@ -201,7 +201,7 @@ def assemble_usfm_by_chapter( book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, fmt_str: str = BOOK_NAME_FMT_STR, -) -> str: +) -> list[str]: """ Construct the HTML wherein at least one USFM resource exists, one column layout. @@ -317,7 +317,7 @@ def rg_sort_key(resource: RGBook) -> str: content.append(rg_verses) content.append(close_direction_html) content.append(end_of_chapter_html) - return "".join(content) + return content def assemble_tn_by_chapter( @@ -332,7 +332,7 @@ def assemble_tn_by_chapter( close_direction_html: str = "", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, -) -> str: +) -> list[str]: """ Construct the HTML for a 'by chapter' strategy wherein at least tn_books exists. @@ -420,7 +420,7 @@ def rg_sort_key(resource: RGBook) -> str: content.append(rg_verses) content.append(close_direction_html) content.append(end_of_chapter_html) - return "".join(content) + return content def assemble_tq_by_chapter( @@ -434,7 +434,7 @@ def assemble_tq_by_chapter( end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, -) -> str: +) -> list[str]: """ Construct the HTML for a 'by chapter' strategy wherein at least tq_books exists. @@ -490,7 +490,7 @@ def rg_sort_key(resource: RGBook) -> str: content.append(rg_verses) content.append(close_direction_html) content.append(end_of_chapter_html) - return "".join(content) + return content # This function could be a little confusing for newcomers. TW lives at @@ -509,7 +509,7 @@ def assemble_tw_by_chapter( rg_books: Sequence[RGBook], use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, -) -> str: +) -> list[str]: content = [] def bc_sort_key(resource: BCBook) -> str: @@ -523,7 +523,7 @@ def bc_sort_key(resource: BCBook) -> str: chapter_commentary(bc_book, chapter_num, use_section_visual_separator) ) content.append(end_of_chapter_html) - return "".join(content) + return content def assemble_usfm_by_chapter_2c_sl_sr( @@ -544,7 +544,7 @@ def assemble_usfm_by_chapter_2c_sl_sr( book_chapters: Mapping[str, int] = BOOK_CHAPTERS, fmt_str: str = BOOK_NAME_FMT_STR, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, -) -> str: +) -> list[str]: """ Construct the HTML for the two column scripture left scripture right layout. @@ -791,4 +791,4 @@ def rg_sort_key(resource: RGBook) -> str: content.append(html_column_end) content.append(html_row_end) content.append(html_row_end) - return "".join(content) + return content diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py index 20847abf..323360f8 100755 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py @@ -44,7 +44,7 @@ def assemble_content_by_lang_then_book( use_section_visual_separator: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, -) -> str: +) -> list[str]: """ Assemble by language then by book in lexicographical order before delegating more atomic ordering/interleaving to an assembly @@ -116,7 +116,7 @@ def assemble_content_by_lang_then_book( ] rg_book = selected_rg_books[0] if selected_rg_books else None if usfm_book is not None: - content.append( + content.extend( assemble_usfm_by_book( usfm_book, tn_book, @@ -129,7 +129,7 @@ def assemble_content_by_lang_then_book( ) ) elif usfm_book is None and tn_book is not None: - content.append( + content.extend( assemble_tn_by_book( usfm_book, tn_book, @@ -142,7 +142,7 @@ def assemble_content_by_lang_then_book( ) ) elif usfm_book is None and tn_book is None and tq_book is not None: - content.append( + content.extend( assemble_tq_by_book( usfm_book, tn_book, @@ -160,7 +160,7 @@ def assemble_content_by_lang_then_book( and tq_book is None and (tw_book is not None or bc_book is not None or rg_book is not None) ): - content.append( + content.extend( assemble_tw_by_book( usfm_book, tn_book, @@ -172,7 +172,7 @@ def assemble_content_by_lang_then_book( use_section_visual_separator, ) ) - return "".join(content) + return content def assemble_usfm_by_book( @@ -188,7 +188,7 @@ def assemble_usfm_by_book( hr: str = "
", close_direction_html: str = "", fmt_str: str = BOOK_NAME_FMT_STR, -) -> str: +) -> list[str]: content = [] content.append(usfm_language_direction_html(usfm_book)) content.append(tn_book_intro(tn_book, use_section_visual_separator)) @@ -234,7 +234,7 @@ def assemble_usfm_by_book( content.append(usfm_book2.chapters[chapter_num].content) content.append(end_of_chapter_html) content.append(close_direction_html) - return "".join(content) + return content def assemble_tn_by_book( @@ -248,7 +248,7 @@ def assemble_tn_by_book( use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", -) -> str: +) -> list[str]: content = [] content.append(tn_language_direction_html(tn_book)) content.append(tn_book_intro(tn_book, use_section_visual_separator)) @@ -272,7 +272,7 @@ def assemble_tn_by_book( ) content.append(end_of_chapter_html) content.append(close_direction_html) - return "".join(content) + return content def assemble_tq_by_book( @@ -286,7 +286,7 @@ def assemble_tq_by_book( use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", -) -> str: +) -> list[str]: content = [] content.append(tq_language_direction_html(tq_book)) if tq_book: @@ -303,7 +303,7 @@ def assemble_tq_by_book( ) content.append(end_of_chapter_html) content.append(close_direction_html) - return "".join(content) + return content def assemble_rg_by_chapter( @@ -316,7 +316,7 @@ def assemble_rg_by_chapter( use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", -) -> str: +) -> list[str]: """ Construct the HTML for a 'by chapter' strategy wherein at least rg_books exists. @@ -360,7 +360,7 @@ def rg_sort_key(resource: RGBook) -> str: ) ) content.append(close_direction_html) - return "".join(content) + return content # It is possible to request only TW, however TW is handled at a @@ -376,7 +376,7 @@ def assemble_tw_by_book( use_section_visual_separator: bool, end_of_chapter_html: str = END_OF_CHAPTER_HTML, close_direction_html: str = "", -) -> str: +) -> list[str]: content = [] if bc_book: for chapter_num in bc_book.chapters: @@ -391,4 +391,4 @@ def assemble_tw_by_book( ) content.append(end_of_chapter_html) - return "".join(content) + return content diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index 75a003ca..f799fd87 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -74,6 +74,99 @@ logger = settings.logger(__name__) +def initialize_document_request_and_key( + document_request_json: str, +) -> tuple[DocumentRequest, str]: + document_request = DocumentRequest.parse_raw(document_request_json) + logger.info("document_request: %s", document_request) + document_request.assembly_layout_kind = select_assembly_layout_kind( + document_request + ) + # Generate the document request key that identifies this and + # identical document requests. + document_request_key_ = document_request_key( + document_request.resource_requests, + document_request.assembly_strategy_kind, + document_request.assembly_layout_kind, + document_request.chunk_size, + document_request.limit_words, + document_request.use_chapter_labels, + document_request.use_section_visual_separator, + ) + return document_request, document_request_key_ + + +def locate_acquire_and_build_resource_objects( + document_request: DocumentRequest, +) -> tuple[ + Sequence[ResourceLookupDto], + Sequence[USFMBook], + Sequence[TNBook], + Sequence[TQBook], + Sequence[TWBook], + Sequence[BCBook], + Sequence[RGBook], +]: + # Update the state of the worker process. This is used by the + # UI to report status. + current_task.update_state(state="Locating assets") + # Docx didn't exist in cache so go ahead and start by getting the + # resource lookup DTOs for each resource request in the document + # request. + resource_lookup_dtos = [] + for resource_request in document_request.resource_requests: + resource_lookup_dto = resource_lookup.resource_lookup_dto( + resource_request.lang_code, + resource_request.resource_type, + resource_request.book_code, + ) + if resource_lookup_dto: + resource_lookup_dtos.append(resource_lookup_dto) + # Determine which resource URLs were actually found. + found_resource_lookup_dtos = [ + resource_lookup_dto + for resource_lookup_dto in resource_lookup_dtos + if resource_lookup_dto.url is not None + ] + # if not found_resource_lookup_dtos: + # raise exceptions.ResourceAssetFileNotFoundError( + # message="No supported resource assets were found" + # ) + current_task.update_state(state="Provisioning asset files") + t0 = time.time() + resource_dirs = [ + resource_lookup.prepare_resource_filepath(dto) + for dto in found_resource_lookup_dtos + ] + for resource_dir, dto in zip(resource_dirs, found_resource_lookup_dtos): + resource_lookup.provision_asset_files(dto.url, resource_dir) + t1 = time.time() + logger.info( + "Time to provision asset files (acquire and write to disk): %s", t1 - t0 + ) + current_task.update_state(state="Parsing asset files") + # Initialize found resources from their provisioned assets. + t0 = time.time() + usfm_books, tn_books, tq_books, tw_books, bc_books, rg_books = parsing.books( + found_resource_lookup_dtos, + resource_dirs, + document_request.resource_requests, + document_request.layout_for_print, + document_request.use_chapter_labels, + ) + t1 = time.time() + logger.info("Time to parse all resource content: %s", t1 - t0) + return ( + found_resource_lookup_dtos, + usfm_books, + tn_books, + tq_books, + tw_books, + bc_books, + rg_books, + ) + + # @worker.app.task( # autoretry_for=(Exception,), # retry_backoff=True, @@ -90,76 +183,23 @@ def generate_document( >>> document_request_json = '{"email_address":null,"assembly_strategy_kind":"lbo","assembly_layout_kind":"1c","layout_for_print":false,"resource_requests":[{"lang_code":"es-419","resource_type":"ulb","book_code":"mat"}],"generate_pdf":true,"generate_epub":false,"generate_docx":false,"chunk_size":"chapter","limit_words":false,"include_tn_book_intros":false,"document_request_source":"ui"}' >>> document_generator.generate_document(document_request_json) """ - logger.info("document_request_json: %s", document_request_json) current_task.update_state(state="Receiving request") - document_request = DocumentRequest.parse_raw(document_request_json) - document_request.assembly_layout_kind = select_assembly_layout_kind( - document_request - ) - # Generate the document request key that identifies this and - # identical document requests. - document_request_key_ = document_request_key( - document_request.resource_requests, - document_request.assembly_strategy_kind, - document_request.assembly_layout_kind, - document_request.chunk_size, - document_request.limit_words, - document_request.use_chapter_labels, - document_request.use_section_visual_separator, + document_request, document_request_key_ = initialize_document_request_and_key( + document_request_json ) html_filepath_ = html_filepath(document_request_key_) pdf_filepath_ = pdf_filepath(document_request_key_) epub_filepath_ = epub_filepath(document_request_key_) if file_needs_update(html_filepath_): - # Update the state of the worker process. This is used by the - # UI to report status. - current_task.update_state(state="Locating assets") - # HTML didn't exist in cache so go ahead and start by getting the - # resource lookup DTOs for each resource request in the document - # request. - resource_lookup_dtos = [] - for resource_request in document_request.resource_requests: - resource_lookup_dto = resource_lookup.resource_lookup_dto( - resource_request.lang_code, - resource_request.resource_type, - resource_request.book_code, - ) - if resource_lookup_dto: - resource_lookup_dtos.append(resource_lookup_dto) - # Determine which resource URLs were actually found. - found_resource_lookup_dtos = [ - resource_lookup_dto - for resource_lookup_dto in resource_lookup_dtos - if resource_lookup_dto.url is not None - ] - # if not found_resource_lookup_dtos: - # raise exceptions.ResourceAssetFileNotFoundError( - # message="No supported resource assets were found" - # ) - current_task.update_state(state="Provisioning asset files") - t0 = time.time() - resource_dirs = [ - resource_lookup.prepare_resource_filepath(dto) - for dto in found_resource_lookup_dtos - ] - for resource_dir, dto in zip(resource_dirs, found_resource_lookup_dtos): - resource_lookup.provision_asset_files(dto.url, resource_dir) - t1 = time.time() - logger.info( - "Time to provision asset files (acquire and write to disk): %s", t1 - t0 - ) - current_task.update_state(state="Parsing asset files") - # Initialize found resources from their provisioned assets. - t0 = time.time() - usfm_books, tn_books, tq_books, tw_books, bc_books, rg_books = parsing.books( + ( found_resource_lookup_dtos, - resource_dirs, - document_request.resource_requests, - document_request.layout_for_print, - document_request.use_chapter_labels, - ) - t1 = time.time() - logger.info("Time to parse all resource content: %s", t1 - t0) + usfm_books, + tn_books, + tq_books, + tw_books, + bc_books, + rg_books, + ) = locate_acquire_and_build_resource_objects(document_request) current_task.update_state(state="Assembling content") content = assemble_content( document_request_key_, @@ -172,12 +212,13 @@ def generate_document( rg_books, found_resource_lookup_dtos, ) + content_str = "".join(content) if usfm_books: - content = check_content_for_issues(content) - content = create_title_page_and_wrap_in_template( - content, document_request, found_resource_lookup_dtos, usfm_books + content_str = check_content_for_issues(content_str) + content_str = create_title_page_and_wrap_in_template( + content_str, document_request, found_resource_lookup_dtos, usfm_books ) - write_html_content_to_file(content, html_filepath_) + write_html_content_to_file(content_str, html_filepath_) else: logger.info("Cache hit for %s", html_filepath_) # Immediately return pre-built PDF if the document has previously been @@ -221,73 +262,22 @@ def generate_docx_document( """ This is the alternative entry point for Docx document creation only. """ - document_request = DocumentRequest.parse_raw(document_request_json) - logger.info( - "document_request: %s", - document_request, - ) - document_request.assembly_layout_kind = select_assembly_layout_kind( - document_request - ) - # Generate the document request key that identifies this and - # identical document requests. - document_request_key_ = document_request_key( - document_request.resource_requests, - document_request.assembly_strategy_kind, - document_request.assembly_layout_kind, - document_request.chunk_size, - document_request.limit_words, - document_request.use_chapter_labels, - document_request.use_section_visual_separator, + current_task.update_state(state="Receiving request") + document_request, document_request_key_ = initialize_document_request_and_key( + document_request_json ) html_filepath_ = html_filepath(document_request_key_) docx_filepath_ = docx_filepath(document_request_key_) if document_request.generate_docx and file_needs_update(docx_filepath_): - # Update the state of the worker process. This is used by the - # UI to report status. - current_task.update_state(state="Locating assets") - # Docx didn't exist in cache so go ahead and start by getting the - # resource lookup DTOs for each resource request in the document - # request. - resource_lookup_dtos = [] - for resource_request in document_request.resource_requests: - resource_lookup_dto = resource_lookup.resource_lookup_dto( - resource_request.lang_code, - resource_request.resource_type, - resource_request.book_code, - ) - if resource_lookup_dto: - resource_lookup_dtos.append(resource_lookup_dto) - # Determine which resource URLs were actually found. - found_resource_lookup_dtos = [ - resource_lookup_dto - for resource_lookup_dto in resource_lookup_dtos - if resource_lookup_dto.url is not None - ] - current_task.update_state(state="Provisioning asset files") - t0 = time.time() - resource_dirs = [ - resource_lookup.prepare_resource_filepath(dto) - for dto in found_resource_lookup_dtos - ] - for resource_dir, dto in zip(resource_dirs, found_resource_lookup_dtos): - resource_lookup.provision_asset_files(dto.url, resource_dir) - t1 = time.time() - logger.info( - "Time to provision asset files (acquire and write to disk): %s", t1 - t0 - ) - current_task.update_state(state="Parsing asset files") - # Initialize found resources from their provisioned assets. - t0 = time.time() - usfm_books, tn_books, tq_books, tw_books, bc_books, rg_books = parsing.books( + ( found_resource_lookup_dtos, - resource_dirs, - document_request.resource_requests, - document_request.layout_for_print, - document_request.use_chapter_labels, - ) - t1 = time.time() - logger.info("Time to parse all resource content: %s", t1 - t0) + usfm_books, + tn_books, + tq_books, + tw_books, + bc_books, + rg_books, + ) = locate_acquire_and_build_resource_objects(document_request) current_task.update_state(state="Assembling content") document_parts = assemble_docx_content( document_request_key_, @@ -456,10 +446,11 @@ def assemble_content( rg_books: Sequence[RGBook], found_resource_lookup_dtos: Sequence[ResourceLookupDto], hr: str = "
", -) -> str: +) -> list[str]: """ Assemble and return the content from all requested resources according to the assembly_strategy requested. + """ t0 = time.time() content = [] @@ -467,7 +458,7 @@ def assemble_content( document_request.assembly_strategy_kind == AssemblyStrategyEnum.LANGUAGE_BOOK_ORDER ): - content.append( + content.extend( assemble_content_by_lang_then_book( usfm_books, tn_books, @@ -483,7 +474,7 @@ def assemble_content( document_request.assembly_strategy_kind == AssemblyStrategyEnum.BOOK_LANGUAGE_ORDER ): - content.append( + content.extend( assemble_content_by_book_then_lang( usfm_books, tn_books, @@ -503,7 +494,7 @@ def assemble_content( for tw_book in tw_books: if tw_book.lang_code not in unique_lang_codes: unique_lang_codes.add(tw_book.lang_code) - content.append( + content.extend( translation_words_section( tw_book, usfm_books, @@ -515,7 +506,7 @@ def assemble_content( content.append(hr) t1 = time.time() logger.info("Time for add TW content to document: %s", t1 - t0) - return "".join(content) + return content def create_title_page_and_wrap_in_template( From 938fd3c393f2f52ae887ebc205f73b8519da2676 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 17 Jul 2025 14:39:32 -0700 Subject: [PATCH 116/208] Reuse common code --- backend/doc/domain/resource_lookup.py | 30 +++++---------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 0506633a..b6d93248 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -852,15 +852,15 @@ def maybe_correct_book_name( def get_book_codes_for_lang( lang_code: str, - resource_assets_dir: str, - book_names: Mapping[str, str], - dcs_mirror_git_username: str, - usfm_resource_types: Sequence[str], - use_localized_book_name: bool, usfm_only: bool = False, check_usfm: bool = False, book_id_map: dict[str, int] = BOOK_ID_MAP, download_assets: bool = settings.DOWNLOAD_ASSETS, + resource_assets_dir: str = settings.RESOURCE_ASSETS_DIR, + book_names: Mapping[str, str] = BOOK_NAMES, + dcs_mirror_git_username: str = "DCS-Mirror", + usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, + use_localized_book_name: bool = settings.USE_LOCALIZED_BOOK_NAME, ) -> Sequence[tuple[str, str]]: data = fetch_source_data() if data is None: @@ -1000,11 +1000,6 @@ def get_book_codes_for_lang( @worker.app.task def book_codes_for_lang( lang_code: str, - resource_assets_dir: str = settings.RESOURCE_ASSETS_DIR, - book_names: Mapping[str, str] = BOOK_NAMES, - dcs_mirror_git_username: str = "DCS-Mirror", - usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, - use_localized_book_name: bool = settings.USE_LOCALIZED_BOOK_NAME, ) -> Sequence[tuple[str, str]]: """ >>> from doc.domain import resource_lookup @@ -1015,11 +1010,6 @@ def book_codes_for_lang( """ return get_book_codes_for_lang( lang_code, - resource_assets_dir, - book_names, - dcs_mirror_git_username, - usfm_resource_types, - use_localized_book_name, usfm_only=False, check_usfm=False, ) @@ -1028,11 +1018,6 @@ def book_codes_for_lang( @worker.app.task def book_codes_for_lang_from_usfm_only( lang_code: str, - resource_assets_dir: str = settings.RESOURCE_ASSETS_DIR, - book_names: Mapping[str, str] = BOOK_NAMES, - dcs_mirror_git_username: str = "DCS-Mirror", - usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, - use_localized_book_name: bool = settings.USE_LOCALIZED_BOOK_NAME, ) -> Sequence[tuple[str, str]]: """ >>> from doc.domain import resource_lookup @@ -1043,11 +1028,6 @@ def book_codes_for_lang_from_usfm_only( """ return get_book_codes_for_lang( lang_code, - resource_assets_dir, - book_names, - dcs_mirror_git_username, - usfm_resource_types, - use_localized_book_name, usfm_only=True, check_usfm=False, ) From df9bbef7ab62cd6ca9c8041a05ab4c018e1ebdcb Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 17 Jul 2025 14:40:07 -0700 Subject: [PATCH 117/208] Handle Burmese, my, irregularity --- backend/doc/domain/resource_lookup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index b6d93248..7bb64611 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -701,6 +701,7 @@ def get_last_segment(url: HttpUrl, lang_code: str) -> str: # lang code -> prefixes to remove LANG_SPECIFIC_PREFIXES_TO_REMOVE = { + "dz": ["Dzongkha_", "chuck_"], "iba-x-ketungau": ["dayakketungau_"], "knx-x-bajanya": ["bajanya_knx"], "ndh": ["chindali_"], @@ -881,7 +882,9 @@ def get_book_codes_for_lang( repo_components = last_segment.split("_") if dcs_mirror_git_username in str(url): repo_components = update_repo_components(repo_components) - if any(rt in str(url) for rt in usfm_resource_types): + if any( + rt in str(url) or rt in last_segment for rt in usfm_resource_types + ): resource_filepath = f"{resource_assets_dir}/{last_segment}" if not any(item[0] == url for item in repo_clone_list): repo_clone_list.append((url, resource_filepath)) From 23a1156f7ad911565affccec060911ccf93b1518 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 17 Jul 2025 14:40:44 -0700 Subject: [PATCH 118/208] Remove unused import --- backend/doc/domain/resource_lookup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 7bb64611..fb0154bc 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -42,7 +42,6 @@ from doc.utils.file_utils import ( delete_tree, file_needs_update, - make_dir, read_file, ) from doc.utils.list_utils import unique_tuples From a19b7f35254820f444d7d99d0b0fa062ce64c047 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 17 Jul 2025 14:41:03 -0700 Subject: [PATCH 119/208] Modify loading string --- frontend/src/routes/resource_types/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/resource_types/+page.svelte b/frontend/src/routes/resource_types/+page.svelte index 03fd49a1..4e01816a 100644 --- a/frontend/src/routes/resource_types/+page.svelte +++ b/frontend/src/routes/resource_types/+page.svelte @@ -181,7 +181,7 @@ {#if ($langCountStore > 0 && (!lang0ResourceTypesAndNames || (lang0ResourceTypesAndNames && lang0ResourceTypesAndNames.length == 0))) || ($langCountStore > 1 && (!lang1ResourceTypesAndNames || (lang1ResourceTypesAndNames && lang1ResourceTypesAndNames.length == 0)))} {:else if windowWidth < TAILWIND_SM_MIN_WIDTH} {#if $langCountStore > 0} From 1e74a51815255d5237738bfb7888811d750fbe11 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 17 Jul 2025 14:41:29 -0700 Subject: [PATCH 120/208] Add timing for HTML to DOCX conversion to log output --- backend/doc/domain/document_generator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index f799fd87..fdd126ec 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -677,6 +677,7 @@ def compose_document( ) -> Document: doc = Document() html_to_docx = HtmlToDocx() + t0 = time.time() for part in document_parts: if part.contained_in_two_column_section: add_two_column_section(doc) @@ -690,6 +691,8 @@ def compose_document( add_hr(doc.paragraphs[-1]) if part.add_page_break: add_page_break(doc) + t1 = time.time() + logger.info("Time for converting HTML to Docx: %s", t1 - t0) return doc From 4774cf1c7666b62014f60cbef6ccbd1ca45cc8d1 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 22 Jul 2025 14:18:49 -0700 Subject: [PATCH 121/208] Remove commented out code --- backend/doc/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/doc/config.py b/backend/doc/config.py index ab6a3378..c02a5366 100755 --- a/backend/doc/config.py +++ b/backend/doc/config.py @@ -18,7 +18,6 @@ class Settings(BaseSettings): (which have higher priority). """ - # GITHUB_API_TOKEN: str = "FOO" # This might be used in a later version DATA_API_URL: HttpUrl USFM_RESOURCE_TYPES: Sequence[str] = [ From 3df93dbc24f0c2edf8b89a532318cdd27deba495 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 22 Jul 2025 14:19:33 -0700 Subject: [PATCH 122/208] Improve source code comment --- backend/doc/domain/bible_books.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/doc/domain/bible_books.py b/backend/doc/domain/bible_books.py index 75a2fede..e1f71709 100644 --- a/backend/doc/domain/bible_books.py +++ b/backend/doc/domain/bible_books.py @@ -75,7 +75,7 @@ } -# Sort the books in canonical order so that groupby does what we want. +# Establish a map by which books can be sorted in bible book order BOOK_ID_MAP = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) BOOK_NUMBERS: Mapping[str, str] = { From 42d505c78921d156bf99d554536e97aedc25e5f5 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 22 Jul 2025 14:21:11 -0700 Subject: [PATCH 123/208] Refactor large function and fix issue The issue fixed is that some languages have book codes because they have non USFM resources associated with those books, but they do not have ny USFM, we still need to show the books available from these non-USFM resources. This was a regression from a few months ago that I didn't discover until testing zh language. --- backend/doc/domain/resource_lookup.py | 291 ++++++++++++++++---------- 1 file changed, 176 insertions(+), 115 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index fb0154bc..9c002823 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -850,28 +850,17 @@ def maybe_correct_book_name( return book_name_ -def get_book_codes_for_lang( +def repos_to_clone( lang_code: str, - usfm_only: bool = False, - check_usfm: bool = False, - book_id_map: dict[str, int] = BOOK_ID_MAP, - download_assets: bool = settings.DOWNLOAD_ASSETS, + augmented_repos_info: list[RepoEntry], resource_assets_dir: str = settings.RESOURCE_ASSETS_DIR, - book_names: Mapping[str, str] = BOOK_NAMES, dcs_mirror_git_username: str = "DCS-Mirror", - usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, - use_localized_book_name: bool = settings.USE_LOCALIZED_BOOK_NAME, -) -> Sequence[tuple[str, str]]: - data = fetch_source_data() - if data is None: - return [] - book_codes_and_names_localized: list[tuple[str, str]] = [] - book_codes_and_names: list[tuple[str, str]] = [] - book_codes_and_names2: list[tuple[str, str]] = [] + resource_type_codes_and_names: Sequence[str] = list( + RESOURCE_TYPE_CODES_AND_NAMES.keys() + ), +) -> list[tuple[HttpUrl, str]]: repo_clone_list: list[tuple[HttpUrl, str]] = [] try: - repos_info = data.git_repo - augmented_repos_info = add_data_not_supplied_by_data_api(repos_info) for repo_info in augmented_repos_info: content = repo_info.content language_info = content.language @@ -882,115 +871,105 @@ def get_book_codes_for_lang( if dcs_mirror_git_username in str(url): repo_components = update_repo_components(repo_components) if any( - rt in str(url) or rt in last_segment for rt in usfm_resource_types + rt in str(url) or rt in last_segment + for rt in resource_type_codes_and_names ): resource_filepath = f"{resource_assets_dir}/{last_segment}" if not any(item[0] == url for item in repo_clone_list): repo_clone_list.append((url, resource_filepath)) - repos_to_clone = [ + except Exception: + logger.exception("Error during repos_to_clone") + finally: + return repo_clone_list + + +def get_book_codes_for_lang( + lang_code: str, + usfm_only: bool = False, + download_assets: bool = settings.DOWNLOAD_ASSETS, +) -> Sequence[tuple[str, str]]: + data = fetch_source_data() + if data is None: + return [] + repo_clone_list: list[tuple[HttpUrl, str]] = [] + try: + repos_info = data.git_repo + augmented_repos_info = add_data_not_supplied_by_data_api(repos_info) + repo_clone_list = repos_to_clone(lang_code, augmented_repos_info) + repos_to_clone_ = [ (url, path) for url, path in repo_clone_list if "en_rg" not in path ] if download_assets: - batch_download_repos(repos_to_clone) + batch_download_repos(repos_to_clone_) else: - batch_clone_git_repos(repos_to_clone) - for url, resource_filepath in repo_clone_list: - for repo_info in augmented_repos_info: - if repo_info.repo_url == url: - last_segment = get_last_segment(url, lang_code) - repo_components = last_segment.split("_") - if ( - len(repo_components) == 2 - and repo_components[-1] in usfm_resource_types - ): - book_codes_and_names_localized = [] - usfm_files = parsing.find_usfm_files(resource_filepath) - for usfm_file in usfm_files: - usfm_file_components = ( - Path(usfm_file).stem.lower().split("-") - ) - book_code = usfm_file_components[1] - resource_type = repo_components[1] - usfm = read_file(usfm_file) if usfm_file else "" - frontmatter, _, _ = parsing.split_usfm_by_chapters( - lang_code, resource_type, book_code, usfm, check_usfm - ) - localized_book_name = parsing.maybe_localized_book_name( - frontmatter - ) - localized_book_name = maybe_correct_book_name( - lang_code, localized_book_name - ) - book_codes_and_names_localized.append( - (book_code, localized_book_name) - ) - break - if ( - use_localized_book_name - and len(repo_components) > 2 - and repo_components[-1] in usfm_resource_types - ): - book_name_file = f"{resource_filepath}/front/title.txt" - if exists(book_name_file): - with open(book_name_file, "r") as fin: - book_name = fin.read() - localized_book_name_ = normalize_localized_book_name( - book_name - ) - localized_book_name = maybe_correct_book_name( - lang_code, localized_book_name_ - ) - book_code = repo_components[1] - book_codes_and_names_localized.append( - ( - book_code, - localized_book_name, - ) - ) - if not usfm_only: - if not book_codes_and_names_localized or any( - name == "" for _, name in book_codes_and_names_localized - ): - if len(repo_components) > 2: - book_code = repo_components[1] - if book_code in book_names: - book_codes_and_names.append( - (book_code, book_names[book_code]) - ) - elif len(repo_components) == 2 and not book_codes_and_names: - if not book_codes_and_names2: - if resource_type in usfm_resource_types: - usfm_files = parsing.find_usfm_files( - resource_filepath - ) - for usfm_file in usfm_files: - book_code = ( - Path(usfm_file) - .stem.lower() - .split("-")[1] - ) - book_codes_and_names2.append( - (book_code, book_names[book_code]) - ) - elif resource_type in ["tn", "tq"]: - subdirs = [ - file - for file in scandir(resource_filepath) - if file.is_dir() and file.name in book_names - ] - for subdir in subdirs: - book_codes_and_names2.append( - ( - subdir.name.lower(), - book_names[subdir.name.lower()], - ) - ) + batch_clone_git_repos(repos_to_clone_) + book_codes_and_names = get_book_codes_for_lang_( + repo_clone_list, + augmented_repos_info, + lang_code, + usfm_only, + ) except Exception: logger.exception("Error during get_book_codes_for_lang") + return book_codes_and_names + + +def get_book_codes_for_lang_( + repo_clone_list: list[tuple[HttpUrl, str]], + augmented_repos_info: list[RepoEntry], + lang_code: str, + usfm_only: bool, + book_names: Mapping[str, str] = BOOK_NAMES, + usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, + use_localized_book_name: bool = settings.USE_LOCALIZED_BOOK_NAME, + book_id_map: dict[str, int] = BOOK_ID_MAP, +) -> list[tuple[str, str]]: + book_codes_and_names_localized: list[tuple[str, str]] = [] + book_codes_and_names: list[tuple[str, str]] = [] + for url, resource_filepath in repo_clone_list: + for repo_info in augmented_repos_info: + if repo_info.repo_url == url: + last_segment = get_last_segment(url, lang_code) + repo_components = last_segment.split("_") + resource_type = repo_components[-1] + if ( + use_localized_book_name + and len(repo_components) == 2 + and resource_type in usfm_resource_types + ): + book_codes_and_names_localized.extend( + get_maybe_localized_book_names_from_usfm_metadata( + resource_filepath, lang_code, resource_type + ) + ) + elif ( + use_localized_book_name + and len(repo_components) > 2 + and resource_type in usfm_resource_types + ): + book_codes_and_names_localized.extend( + get_book_name_from_title_file( + resource_filepath, + lang_code, + repo_components, + ) + ) + if not usfm_only and ( + not book_codes_and_names_localized + or any(name == "" for _, name in book_codes_and_names_localized) + ): # We can get book names from TN and TQ resources too if no USFM was + # available and we ask for it. No localized book name sources were + # found, so use other alternatives for book name lookup + book_codes_and_names = get_non_localized_book_names( + repo_components, + book_names, + resource_type, + usfm_resource_types, + resource_filepath, + ) if not book_codes_and_names_localized or any( name == "" for _, name in book_codes_and_names_localized ): - book_codes_and_names.extend(book_codes_and_names2) unique_values = unique_tuples(book_codes_and_names) else: unique_values = unique_tuples(book_codes_and_names_localized) @@ -999,6 +978,90 @@ def get_book_codes_for_lang( ) +def get_non_localized_book_names( + repo_components: list[str], + book_names: Mapping[str, str], + resource_type: str, + usfm_resource_types: Sequence[str], + resource_filepath: str, +) -> list[tuple[str, str]]: + book_codes_and_names: list[tuple[str, str]] = [] + book_codes_and_names2: list[tuple[str, str]] = [] + if len(repo_components) > 2: + # Get book code from repo URL components and then lookup in English book names + book_code = repo_components[1] + if book_code in book_names: + book_codes_and_names.append((book_code, book_names[book_code])) + elif len(repo_components) == 2 and not book_codes_and_names: + if ( + not book_codes_and_names2 + ): # TODO Is this needed any longer now that local var is used? + # Get book code from USFM file name and then lookup name in English book names + if resource_type in usfm_resource_types: + usfm_files = parsing.find_usfm_files(resource_filepath) + for usfm_file in usfm_files: + book_code = Path(usfm_file).stem.lower().split("-")[1] + book_codes_and_names2.append((book_code, book_names[book_code])) + elif resource_type in ["tn", "tq"]: + # Get book code from TN and TQ repo book sub-directory names and use to lookup in English book names + subdirs = [ + file + for file in scandir(resource_filepath) + if file.is_dir() and file.name in book_names + ] + for subdir in subdirs: + book_codes_and_names2.append( + ( + subdir.name.lower(), + book_names[subdir.name.lower()], + ) + ) + book_codes_and_names.extend(book_codes_and_names2) + return book_codes_and_names + + +def get_book_name_from_title_file( + resource_filepath: str, + lang_code: str, + repo_components: list[str], +) -> list[tuple[str, str]]: + book_codes_and_names_localized: list[tuple[str, str]] = [] + book_name_file = join(resource_filepath, "front", "title.txt") + if exists(book_name_file): + with open(book_name_file, "r") as fin: + book_name = fin.read() + localized_book_name_ = normalize_localized_book_name(book_name) + localized_book_name = maybe_correct_book_name( + lang_code, localized_book_name_ + ) + book_code = repo_components[1] + book_codes_and_names_localized.append( + ( + book_code, + localized_book_name, + ) + ) + return book_codes_and_names_localized + + +def get_maybe_localized_book_names_from_usfm_metadata( + resource_filepath: str, lang_code: str, resource_type: str +) -> list[tuple[str, str]]: + book_codes_and_names_localized = [] + usfm_files = parsing.find_usfm_files(resource_filepath) + for usfm_file in usfm_files: + usfm_file_components = Path(usfm_file).stem.lower().split("-") + book_code = usfm_file_components[1] + usfm = read_file(usfm_file) if usfm_file else "" + frontmatter, _, _ = parsing.split_usfm_by_chapters( + lang_code, resource_type, book_code, usfm + ) + localized_book_name = parsing.maybe_localized_book_name(frontmatter) + localized_book_name = maybe_correct_book_name(lang_code, localized_book_name) + book_codes_and_names_localized.append((book_code, localized_book_name)) + return book_codes_and_names_localized + + @worker.app.task def book_codes_for_lang( lang_code: str, @@ -1013,7 +1076,6 @@ def book_codes_for_lang( return get_book_codes_for_lang( lang_code, usfm_only=False, - check_usfm=False, ) @@ -1031,7 +1093,6 @@ def book_codes_for_lang_from_usfm_only( return get_book_codes_for_lang( lang_code, usfm_only=True, - check_usfm=False, ) From b98237d2f8cc3671e2b15ccbd8af2a96ca71b094 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 25 Jul 2025 12:49:22 -0700 Subject: [PATCH 124/208] Refactor large function continued --- backend/doc/domain/resource_lookup.py | 87 ++++++++++++++++----------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 9c002823..57325391 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -938,8 +938,10 @@ def get_book_codes_for_lang_( and resource_type in usfm_resource_types ): book_codes_and_names_localized.extend( - get_maybe_localized_book_names_from_usfm_metadata( - resource_filepath, lang_code, resource_type + get_book_names_from_usfm_metadata( + resource_filepath, + lang_code, + resource_type, ) ) elif ( @@ -960,12 +962,14 @@ def get_book_codes_for_lang_( ): # We can get book names from TN and TQ resources too if no USFM was # available and we ask for it. No localized book name sources were # found, so use other alternatives for book name lookup - book_codes_and_names = get_non_localized_book_names( - repo_components, - book_names, - resource_type, - usfm_resource_types, - resource_filepath, + book_codes_and_names.extend( + get_non_localized_book_names( + repo_components, + book_names, + resource_type, + usfm_resource_types, + resource_filepath, + ) ) if not book_codes_and_names_localized or any( name == "" for _, name in book_codes_and_names_localized @@ -985,38 +989,38 @@ def get_non_localized_book_names( usfm_resource_types: Sequence[str], resource_filepath: str, ) -> list[tuple[str, str]]: + """ + Get English book names + """ book_codes_and_names: list[tuple[str, str]] = [] - book_codes_and_names2: list[tuple[str, str]] = [] if len(repo_components) > 2: # Get book code from repo URL components and then lookup in English book names book_code = repo_components[1] if book_code in book_names: book_codes_and_names.append((book_code, book_names[book_code])) - elif len(repo_components) == 2 and not book_codes_and_names: - if ( - not book_codes_and_names2 - ): # TODO Is this needed any longer now that local var is used? - # Get book code from USFM file name and then lookup name in English book names - if resource_type in usfm_resource_types: - usfm_files = parsing.find_usfm_files(resource_filepath) - for usfm_file in usfm_files: - book_code = Path(usfm_file).stem.lower().split("-")[1] - book_codes_and_names2.append((book_code, book_names[book_code])) - elif resource_type in ["tn", "tq"]: - # Get book code from TN and TQ repo book sub-directory names and use to lookup in English book names - subdirs = [ - file - for file in scandir(resource_filepath) - if file.is_dir() and file.name in book_names - ] - for subdir in subdirs: - book_codes_and_names2.append( - ( - subdir.name.lower(), - book_names[subdir.name.lower()], - ) + elif len(repo_components) == 2: + # if resource_type in usfm_resource_types: + # logger.debug("FUBAR") # DEBUG This case happened + # # Get book code from USFM file name and then lookup name in English book names + # usfm_files = parsing.find_usfm_files(resource_filepath) + # for usfm_file in usfm_files: + # book_code = Path(usfm_file).stem.lower().split("-")[1] + # book_codes_and_names.append((book_code, book_names[book_code])) + if resource_type in ["tn", "tq"]: + # Get book code from TN and TQ repo book sub-directory + # names and use to lookup in English book names + subdirs = [ + file + for file in scandir(resource_filepath) + if file.is_dir() and file.name in book_names + ] + for subdir in subdirs: + book_codes_and_names.append( + ( + subdir.name.lower(), + book_names[subdir.name.lower()], ) - book_codes_and_names.extend(book_codes_and_names2) + ) return book_codes_and_names @@ -1025,6 +1029,10 @@ def get_book_name_from_title_file( lang_code: str, repo_components: list[str], ) -> list[tuple[str, str]]: + """ + Book names in front/title.txt files may or may not be localized, + it depends on the translation work done for lang_code. + """ book_codes_and_names_localized: list[tuple[str, str]] = [] book_name_file = join(resource_filepath, "front", "title.txt") if exists(book_name_file): @@ -1044,10 +1052,17 @@ def get_book_name_from_title_file( return book_codes_and_names_localized -def get_maybe_localized_book_names_from_usfm_metadata( - resource_filepath: str, lang_code: str, resource_type: str +def get_book_names_from_usfm_metadata( + resource_filepath: str, + lang_code: str, + resource_type: str, ) -> list[tuple[str, str]]: - book_codes_and_names_localized = [] + """ + Book names obtained from USFM frontmatter/metadata may or may not + be localized, it depends on the translation work done for language + lang_code. + """ + book_codes_and_names_localized: list[tuple[str, str]] = [] usfm_files = parsing.find_usfm_files(resource_filepath) for usfm_file in usfm_files: usfm_file_components = Path(usfm_file).stem.lower().split("-") From b75f8241a9054bd237546c8b7a9adef3ade13b15 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 25 Jul 2025 12:46:13 -0700 Subject: [PATCH 125/208] Remove unneeded cmd --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1a51a99e..17355573 100644 --- a/Dockerfile +++ b/Dockerfile @@ -117,4 +117,3 @@ USER appuser EXPOSE 8000 # Command to run the application -CMD ["python", "backend/main.py"] From cc793605c7254690ca8e975598b57bf3774f72f3 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 29 Jul 2025 13:22:13 -0700 Subject: [PATCH 126/208] Update a few source code comments --- Dockerfile | 2 -- backend/doc/domain/document_generator.py | 13 +++++-------- tests/e2e/doc/test_api.py | 1 - 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 17355573..07f17c98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -115,5 +115,3 @@ USER appuser # Expose necessary ports (if any) EXPOSE 8000 - -# Command to run the application diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index fdd126ec..06696ac6 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -607,10 +607,13 @@ def assemble_docx_content( # HTML to PDF converters: -# princexml ($$$$) (fastest); also available through docraptor api ($$) but slow, +# princexml ($$$$ or non-commercial with watermark) (fastest); we use +# with non-commercial license (watermark on 1st page of pdf). Handles +# layout flawlessly and is dramatically faster than weasyprint and +# all other solutions. +# weasyprint (does a nice job, we also use this), # wkhtmltopdf via pdfkit (can't handle column-count directive so can't use due to # multi-column layouts requirement), -# weasyprint (does a nice job, we use this), # pagedjs-cli (does a really nice job, but is really slow - uses puppeteer underneath), # electron-pdf (similar speed to wkhtmltopdf) which uses chrome underneath the hood, # gotenburg which uses chrome under the hood and provides a nice api in Docker (untested), @@ -627,12 +630,6 @@ def convert_html_to_pdf( assert exists(html_filepath) logger.info("Generating PDF %s...", pdf_filepath) t0 = time.time() - # command = [ - # "ebook-convert", - # html_filepath, - # pdf_filepath, - # "--disable-font-rescaling", - # ] command = [ "weasyprint", html_filepath, diff --git a/tests/e2e/doc/test_api.py b/tests/e2e/doc/test_api.py index 6bbb7997..95c73479 100644 --- a/tests/e2e/doc/test_api.py +++ b/tests/e2e/doc/test_api.py @@ -54,7 +54,6 @@ def test_en_ulb_col_en_tn_col_language_book_order_with_no_email_1c_docx() -> Non check_result(response, suffix="docx") -# @pytest.mark.skip @pytest.mark.skip def test_en_ulb_col_en_tn_col_language_book_order_with_no_email_1c_c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: From 76f8cc7236515f5229ff7dfaf712071439003725 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 29 Jul 2025 13:30:10 -0700 Subject: [PATCH 127/208] Update doctests so that they all pass again --- backend/doc/domain/resource_lookup.py | 31 ++++++++++++++++++++------- backend/doc/utils/list_utils.py | 14 ++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 57325391..c6aa264a 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -44,7 +44,7 @@ file_needs_update, read_file, ) -from doc.utils.list_utils import unique_tuples +from doc.utils.list_utils import unique_tuples, unique_book_codes from doc.utils.text_utils import normalize_localized_book_name from fastapi import HTTPException, status from pydantic import HttpUrl @@ -213,7 +213,7 @@ def fetch_source_data( >>> ();result = resource_lookup.fetch_source_data();() # doctest: +ELLIPSIS (...) >>> result.git_repo[0] - RepoEntry(repo_url=HttpUrl('https://content.bibletranslationtools.org/mmandarri/acz_1jn_text_reg'), content=Content(resource_type='reg', language=Language(english_name='Garme', ietf_code='acz', national_name='Garme', direction=))) + RepoEntry(repo_url=HttpUrl('https://content.bibletranslationtools.org/JohnAngilla/acz_phm_text_reg'), content=Content(resource_type='reg', language=Language(english_name='Garme', ietf_code='acz', national_name='Garme', direction=))) """ graphql_query = """ query MyQuery { @@ -349,6 +349,8 @@ def resource_types( download_assets: bool = settings.DOWNLOAD_ASSETS, ) -> Sequence[tuple[str, str]]: """ + Fetches and processes available resource types for the given language and book codes. + >>> from doc.domain import resource_lookup >>> lang_code = "pt-br" >>> books = resource_lookup.book_codes_for_lang(lang_code) @@ -356,7 +358,6 @@ def resource_types( (...) >>> result [('blv', 'Portuguese Bíblia Livre'), ('tw', 'Translation Words'), ('ulb', 'Unlocked Literal Bible')] - Fetches and processes available resource types for the given language and book codes. """ book_codes = book_codes_str.split(",") if book_codes and book_codes[0] == "all": @@ -615,16 +616,23 @@ def usfm_resource_types_and_book_tuples( def shared_book_codes(lang0_code: str, lang1_code: str) -> Sequence[tuple[str, str]]: """ - Given two language codes, return the intersection of resource + Given two language codes, return the intersection of book codes between the two languages. >>> from doc.domain import resource_lookup >>> # Hack to ignore logging output: https://stackoverflow.com/a/33400983/3034580 + >>> ();data = resource_lookup.book_codes_for_lang("pt-br");() # doctest: +ELLIPSIS + (...) + >>> list(data) + [('gen', 'Gênesis'), ('exo', 'Êxodo'), ('lev', 'Levítico'), ('num', 'Números'), ('deu', 'Deuteronômio'), ('jos', 'Josué'), ('jdg', 'Juízes'), ('rut', 'Rute'), ('1sa', '1 Samuel'), ('2sa', '2 Samuel'), ('1ki', '1 Reis'), ('2ki', '2 Reis'), ('1ch', '1 Crônicas'), ('2ch', '2 Crônicas'), ('ezr', 'Esdras'), ('neh', 'Neemias'), ('est', 'Ester'), ('job', 'Jó'), ('psa', 'Salmos'), ('pro', 'Provérbios'), ('ecc', 'Eclesiastes'), ('sng', 'Cantares'), ('isa', 'Isaías'), ('jer', 'Jeremias'), ('lam', 'Lamentações'), ('ezk', 'Ezequiel'), ('dan', 'Daniel'), ('hos', 'Oseias'), ('jol', 'Joel'), ('amo', 'Amós'), ('oba', 'Obadias'), ('jon', 'Jonas'), ('mic', 'Miqueias'), ('nam', 'Naum'), ('hab', 'Habacuque'), ('zep', 'Sofonias'), ('hag', 'Ageu'), ('zec', 'Zacarias'), ('mal', 'Malaquias'), ('mat', 'Mateus'), ('mrk', 'Marcos'), ('luk', 'Lucas'), ('jhn', 'João'), ('act', 'Atos'), ('rom', 'Romanos'), ('1co', '1 Coríntios'), ('2co', '2 Coríntios'), ('gal', 'Gálatas'), ('eph', 'Efésios'), ('php', 'Filipenses'), ('col', 'Colossenses'), ('1th', '1 Tessalonicenses'), ('2th', '2 Tessalonicenses'), ('1ti', '1 Timóteo'), ('2ti', '2 Timóteo'), ('tit', 'Tito'), ('phm', 'Filemom'), ('heb', 'Hebreus'), ('jas', 'Tiago'), ('1pe', '1 Pedro'), ('2pe', '2 Pedro'), ('1jn', '1 João'), ('2jn', '2 João'), ('3jn', '3 João'), ('jud', 'Judas'), ('rev', 'Apocalipse')] + >>> ();data = resource_lookup.book_codes_for_lang("es-419");() # doctest: +ELLIPSIS + (...) + >>> list(data) + [('gen', 'Génesis'), ('exo', 'Éxodo'), ('lev', 'Levítico'), ('num', 'Números'), ('deu', 'Deuteronomio'), ('jos', 'Josué'), ('jdg', 'Jueces'), ('rut', 'Ruth'), ('1sa', '1 Samuel'), ('2sa', '2 Samuel'), ('1ki', '1 Reyes'), ('2ki', '2 Reyes'), ('1ch', '1 Crónicas'), ('2ch', '2 Crónicas'), ('ezr', 'Esdras'), ('neh', 'Nehemías'), ('est', 'Ester'), ('job', 'Job'), ('psa', 'Salmos'), ('pro', 'Proverbios'), ('ecc', 'Eclesiastés'), ('sng', 'Cántico de salomón'), ('isa', 'Isaías'), ('jer', 'Jeremías'), ('lam', 'Lamentaciones'), ('ezk', 'Ezequiel'), ('dan', 'Daniel'), ('hos', 'Oseas'), ('jol', 'Joel'), ('amo', 'Amós'), ('oba', 'Abdías'), ('jon', 'Jonás'), ('mic', 'Miqueas'), ('nam', 'Nahúm'), ('hab', 'Habacuc'), ('zep', 'Sofonías'), ('hag', 'Hageo'), ('zec', 'Zacarías'), ('mal', 'Malaquías'), ('mat', 'Mateo'), ('mrk', 'Marcos'), ('luk', 'Lucas'), ('jhn', 'Juan'), ('act', 'Hechos'), ('rom', 'Romanos'), ('1co', '1 Corintios'), ('2co', '2 Corintios'), ('gal', 'Gálatas'), ('eph', 'Efesios'), ('php', 'Filipenses'), ('col', 'Colosenses'), ('1th', '1 Tesalonicenses'), ('2th', '2 Tesalonicenses'), ('1ti', '1 Timoteo'), ('2ti', '2 Timoteo'), ('tit', 'Tito'), ('phm', 'Filemón'), ('heb', 'Hebreos'), ('jas', 'Santiago'), ('1pe', '1 Pedro'), ('2pe', '2 Pedro'), ('1jn', '1 Juan'), ('2jn', '2 Juan'), ('3jn', '3 Juan'), ('jud', 'Judas'), ('rev', 'Apocalipsis')] >>> ();data = resource_lookup.shared_book_codes("pt-br", "es-419");() # doctest: +ELLIPSIS (...) >>> list(data) - [('gen', 'Gênesis'), ('exo', 'Êxodo'), ('lev', 'Levíticos'), ('num', 'Números'), ('deu', 'Deuteronômio'), ('jos', 'Josué'), ('jdg', 'Juízes'), ('rut', 'Rute'), ('1sa', '1 Samuel'), ('2sa', '2 Samuel'), ('1ki', '1 Reis'), ('2ki', '2 Reis'), ('1ch', '1 Crônicas'), ('2ch', '2 Crônicas'), ('ezr', 'Esdras'), ('neh', 'Neemias'), ('est', 'Ester'), ('job', 'Jó'), ('psa', 'Salmos'), ('pro', 'Provérbios'), ('ecc', 'Eclesiastes'), ('sng', 'Cantares de salomão'), ('isa', 'Isaías'), ('jer', 'Jeremias'), ('lam', 'Lamentações'), ('ezk', 'Ezequiel'), ('dan', 'Daniel'), ('hos', 'Oseias'), ('jol', 'Joel'), ('amo', 'Amós'), ('oba', 'Obadias'), ('jon', 'Jonas'), ('mic', 'Miqueias'), ('nam', 'Naum'), ('hab', 'Habacuque'), ('zep', 'Sofonias'), ('hag', 'Ageu'), ('zec', 'Zacarias'), ('mal', 'Malaquias'), ('mat', 'Mateus'), ('mrk', 'Marcos'), ('luk', 'Lucas'), ('jhn', 'João'), ('act', 'Atos'), ('rom', 'Romanos'), ('1co', '1 Coríntios'), ('2co', '2 Coríntios'), ('gal', 'Gálatas'), ('eph', 'Efésios'), ('php', 'Filipenses'), ('col', 'Colossenses'), ('1th', '1 Tessalonicenses'), ('2th', '2 Tessalonicenses'), ('1ti', '1 Timóteo'), ('2ti', '2 Timóteo'), ('tit', 'Tito'), ('phm', 'Filemom'), ('heb', 'Hebreus'), ('jas', 'Tiago'), ('1pe', '1 Pedro'), ('2pe', '2 Pedro'), ('1jn', '1 João'), ('2jn', '2 João'), ('3jn', '3 João'), ('jud', 'Judas'), ('rev', 'Apocalipse')] - + [('gen', 'Gênesis'), ('exo', 'Êxodo'), ('lev', 'Levítico'), ('num', 'Números'), ('deu', 'Deuteronômio'), ('jos', 'Josué'), ('jdg', 'Juízes'), ('rut', 'Rute'), ('1sa', '1 Samuel'), ('2sa', '2 Samuel'), ('1ki', '1 Reis'), ('2ki', '2 Reis'), ('1ch', '1 Crônicas'), ('2ch', '2 Crônicas'), ('ezr', 'Esdras'), ('neh', 'Neemias'), ('est', 'Ester'), ('job', 'Jó'), ('psa', 'Salmos'), ('pro', 'Provérbios'), ('ecc', 'Eclesiastes'), ('sng', 'Cantares'), ('isa', 'Isaías'), ('jer', 'Jeremias'), ('lam', 'Lamentações'), ('ezk', 'Ezequiel'), ('dan', 'Daniel'), ('hos', 'Oseias'), ('jol', 'Joel'), ('amo', 'Amós'), ('oba', 'Obadias'), ('jon', 'Jonas'), ('mic', 'Miqueias'), ('nam', 'Naum'), ('hab', 'Habacuque'), ('zep', 'Sofonias'), ('hag', 'Ageu'), ('zec', 'Zacarias'), ('mal', 'Malaquias'), ('mat', 'Mateus'), ('mrk', 'Marcos'), ('luk', 'Lucas'), ('jhn', 'João'), ('act', 'Atos'), ('rom', 'Romanos'), ('1co', '1 Coríntios'), ('2co', '2 Coríntios'), ('gal', 'Gálatas'), ('eph', 'Efésios'), ('php', 'Filipenses'), ('col', 'Colossenses'), ('1th', '1 Tessalonicenses'), ('2th', '2 Tessalonicenses'), ('1ti', '1 Timóteo'), ('2ti', '2 Timóteo'), ('tit', 'Tito'), ('phm', 'Filemom'), ('heb', 'Hebreus'), ('jas', 'Tiago'), ('1pe', '1 Pedro'), ('2pe', '2 Pedro'), ('1jn', '1 João'), ('2jn', '2 João'), ('3jn', '3 João'), ('jud', 'Judas'), ('rev', 'Apocalipse')] """ lang0_book_codes = book_codes_for_lang(lang0_code) lang1_book_codes = book_codes_for_lang(lang1_code) @@ -888,6 +896,13 @@ def get_book_codes_for_lang( usfm_only: bool = False, download_assets: bool = settings.DOWNLOAD_ASSETS, ) -> Sequence[tuple[str, str]]: + """ + >>> from doc.domain.resource_lookup import book_codes_for_lang + >>> book_codes_for_lang("zh") # zh doesn't have USFM resource available, get books from non-USFM resources + [('gen', 'Genesis'), ('exo', 'Exodus'), ('lev', 'Leviticus'), ('num', 'Numbers'), ('deu', 'Deuteronomy'), ('jos', 'Joshua'), ('jdg', 'Judges'), ('rut', 'Ruth'), ('1sa', '1 Samuel'), ('2sa', '2 Samuel'), ('1ki', '1 Kings'), ('2ki', '2 Kings'), ('1ch', '1 Chronicles'), ('2ch', '2 Chronicles'), ('ezr', 'Ezra'), ('neh', 'Nehemiah'), ('est', 'Esther'), ('job', 'Job'), ('psa', 'Psalms'), ('pro', 'Proverbs'), ('ecc', 'Ecclesiastes'), ('sng', 'Song of Solomon'), ('isa', 'Isaiah'), ('jer', 'Jeremiah'), ('lam', 'Lamentations'), ('ezk', 'Ezekiel'), ('dan', 'Daniel'), ('hos', 'Hosea'), ('jol', 'Joel'), ('amo', 'Amos'), ('oba', 'Obadiah'), ('jon', 'Jonah'), ('mic', 'Micah'), ('nam', 'Nahum'), ('hab', 'Habakkuk'), ('zep', 'Zephaniah'), ('hag', 'Haggai'), ('zec', 'Zechariah'), ('mal', 'Malachi'), ('mat', 'Matthew'), ('mrk', 'Mark'), ('luk', 'Luke'), ('jhn', 'John'), ('act', 'Acts'), ('rom', 'Romans'), ('1co', '1 Corinthians'), ('2co', '2 Corinthians'), ('gal', 'Galatians'), ('eph', 'Ephesians'), ('php', 'Philippians'), ('col', 'Colossians'), ('1th', '1 Thessalonians'), ('2th', '2 Thessalonians'), ('1ti', '1 Timothy'), ('2ti', '2 Timothy'), ('tit', 'Titus'), ('phm', 'Philemon'), ('heb', 'Hebrews'), ('jas', 'James'), ('1pe', '1 Peter'), ('2pe', '2 Peter'), ('1jn', '1 John'), ('2jn', '2 John'), ('3jn', '3 John'), ('jud', 'Jude'), ('rev', 'Revelation')] + >>> book_codes_for_lang("pt-br") # pt-br has, for example, two book names for lev + [('gen', 'Gênesis'), ('exo', 'Êxodo'), ('lev', 'Levítico'), ('num', 'Números'), ('deu', 'Deuteronômio'), ('jos', 'Josué'), ('jdg', 'Juízes'), ('rut', 'Rute'), ('1sa', '1 Samuel'), ('2sa', '2 Samuel'), ('1ki', '1 Reis'), ('2ki', '2 Reis'), ('1ch', '1 Crônicas'), ('2ch', '2 Crônicas'), ('ezr', 'Esdras'), ('neh', 'Neemias'), ('est', 'Ester'), ('job', 'Jó'), ('psa', 'Salmos'), ('pro', 'Provérbios'), ('ecc', 'Eclesiastes'), ('sng', 'Cantares'), ('isa', 'Isaías'), ('jer', 'Jeremias'), ('lam', 'Lamentações'), ('ezk', 'Ezequiel'), ('dan', 'Daniel'), ('hos', 'Oseias'), ('jol', 'Joel'), ('amo', 'Amós'), ('oba', 'Obadias'), ('jon', 'Jonas'), ('mic', 'Miqueias'), ('nam', 'Naum'), ('hab', 'Habacuque'), ('zep', 'Sofonias'), ('hag', 'Ageu'), ('zec', 'Zacarias'), ('mal', 'Malaquias'), ('mat', 'Mateus'), ('mrk', 'Marcos'), ('luk', 'Lucas'), ('jhn', 'João'), ('act', 'Atos'), ('rom', 'Romanos'), ('1co', '1 Coríntios'), ('2co', '2 Coríntios'), ('gal', 'Gálatas'), ('eph', 'Efésios'), ('php', 'Filipenses'), ('col', 'Colossenses'), ('1th', '1 Tessalonicenses'), ('2th', '2 Tessalonicenses'), ('1ti', '1 Timóteo'), ('2ti', '2 Timóteo'), ('tit', 'Tito'), ('phm', 'Filemom'), ('heb', 'Hebreus'), ('jas', 'Tiago'), ('1pe', '1 Pedro'), ('2pe', '2 Pedro'), ('1jn', '1 João'), ('2jn', '2 João'), ('3jn', '3 João'), ('jud', 'Judas'), ('rev', 'Apocalipse')] + """ data = fetch_source_data() if data is None: return [] @@ -974,9 +989,9 @@ def get_book_codes_for_lang_( if not book_codes_and_names_localized or any( name == "" for _, name in book_codes_and_names_localized ): - unique_values = unique_tuples(book_codes_and_names) + unique_values = unique_book_codes(book_codes_and_names) else: - unique_values = unique_tuples(book_codes_and_names_localized) + unique_values = unique_book_codes(book_codes_and_names_localized) return sorted( unique_values, key=lambda book_code_and_name: book_id_map[book_code_and_name[0]] ) diff --git a/backend/doc/utils/list_utils.py b/backend/doc/utils/list_utils.py index c3c9a02e..691874c9 100644 --- a/backend/doc/utils/list_utils.py +++ b/backend/doc/utils/list_utils.py @@ -11,3 +11,17 @@ def unique_tuples(lst: list[T]) -> list[T]: >>> assert unique_tuples(data_3) == [('x', 'y', True), ('x', 'y', False), ('a', 'b', True)] """ return list(dict.fromkeys(lst)) + + +def unique_book_codes(lst: list[T]) -> list[T]: + """ + >>> input_list = [("lev", "value1"), ("lev", "value2", True), ("abc", "value3"), ("abc", "value4", False)] + >>> result = unique_tuples(input_list) + [('lev', 'value1'), ('abc', 'value3')] + """ + seen = {} + for item in lst: + key = item[0] + if key not in seen: + seen[key] = item + return list(seen.values()) From 8040bc0ca2ed067bcd2e0e7c481776521d5b94c6 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 29 Jul 2025 13:31:18 -0700 Subject: [PATCH 128/208] Add PrinceXml as HTML to PDF converter choice for api & user User can choose to use the faster and better HTML to PDF converter PrinceXml as long as they are OK with it putting a PrinceXml logo on the first page of the PDF (as per PrinceXml non-commercial license). * Default to not using PrinceXML for HTML to PDF conversion The assumption here (which may change later) is that users won't want the Prince PDF logo on the first page of the PDF (which is required by the non-commercial license). * Add backend test for using princexml * Update frontend tests and add a frontend test for princexml --- Dockerfile | 33 +++++- backend/doc/domain/document_generator.py | 24 +++- backend/doc/domain/model.py | 15 ++- frontend/src/lib/stores/SettingsStore.ts | 1 + frontend/src/routes/settings/+page.svelte | 16 ++- .../routes/settings/GenerateDocument.svelte | 6 +- frontend/tests/e2e/passages_test.ts | 110 +++++++++--------- frontend/tests/e2e/test.ts | 34 +++++- tests/e2e/doc/test_use_prince.py | 39 +++++++ 9 files changed, 211 insertions(+), 67 deletions(-) create mode 100644 tests/e2e/doc/test_use_prince.py diff --git a/Dockerfile b/Dockerfile index 07f17c98..99bac877 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,38 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # For weasyprint pango1.0-tools \ # For fc-cache - fontconfig + fontconfig \ + # For princexml + fonts-khmeros \ + fonts-lklug-sinhala \ + fonts-tlwg-garuda-otf \ + fonts-lohit-orya \ + fonts-lohit-mlym \ + fonts-lohit-knda \ + fonts-lohit-telu \ + fonts-lohit-taml \ + fonts-lohit-gujr \ + fonts-lohit-guru \ + fonts-lohit-beng-bengali \ + fonts-lohit-deva \ + fonts-baekmuk \ + fonts-ipafont-mincho \ + fonts-arphic-uming \ + fonts-opensymbol \ + fonts-liberation2 \ + libaom3 \ + libavif15 \ + libgif7 \ + libjpeg62-turbo \ + liblcms2-2 \ + libtiff6 \ + libwebp7 \ + libwebpdemux2 + + +# Download the PrinceXML .deb file +RUN wget https://www.princexml.com/download/prince_16.1-1_debian12_amd64.deb \ + && dpkg -i prince_16.1-1_debian12_amd64.deb || apt-get install -fy # Get and install needed fonts. RUN cd /tmp \ diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index 06696ac6..5e80c4a4 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -225,7 +225,12 @@ def generate_document( # generated and is fresh enough. if document_request.generate_pdf and file_needs_update(pdf_filepath_): current_task.update_state(state="Converting to PDF") - convert_html_to_pdf(html_filepath_, pdf_filepath_, document_request_key_) + convert_html_to_pdf( + html_filepath_, + pdf_filepath_, + document_request_key_, + document_request.use_prince, + ) if should_send_email(document_request.email_address): attachments = [ Attachment(filepath=pdf_filepath_, mime_type=("application", "pdf")) @@ -623,6 +628,9 @@ def convert_html_to_pdf( html_filepath: str, pdf_filepath: str, document_request_key: str, + use_prince: bool, + default_converter: str = "weasyprint", + alternative_converter: str = "prince", ) -> None: """ Generate PDF from HTML and copy it to output directory. @@ -630,11 +638,15 @@ def convert_html_to_pdf( assert exists(html_filepath) logger.info("Generating PDF %s...", pdf_filepath) t0 = time.time() - command = [ - "weasyprint", - html_filepath, - pdf_filepath, - ] + if use_prince: + command = [ + alternative_converter, + html_filepath, + "-o", + pdf_filepath, + ] + else: + command = [default_converter, html_filepath, pdf_filepath] logger.info("Generate PDF command: %s", " ".join(command)) subprocess.run( command, diff --git a/backend/doc/domain/model.py b/backend/doc/domain/model.py index ef6c644c..9e99114f 100644 --- a/backend/doc/domain/model.py +++ b/backend/doc/domain/model.py @@ -201,7 +201,20 @@ class DocumentRequest(BaseModel): # is True, then the chapter label will be localized to the language(s) # requested. use_chapter_labels: bool = False - # Indiciate whether to show visual separator between sections, e.g., hr element + # We have two PDF processors that we can use: Weasyprint and Princexml. + # Weasyprint is an open source project and produces decent PDFs, but is + # slow and doesn't always obey the HTML and CSS formatting exactly, + # e.g., sometimes text is flowed around chapter numbers as it should be + # and sometimes it isn't (when CSS dictates it). It is a bug in + # weasyprint. Princexml is a top of class closed source solution. + # Princexml is the fastest HTML to PDF converter known and its fidelity + # to CSS is best in class. We are able to use Princexml under a + # non-commercial license since it leaves a Princexml logo on the first + # page of the generated PDF. The logo isn't too obtrusive. One may + # choose to use Princexml rather than Weasyprint by setting use_prince + # to True. + use_prince: bool = False + # Indicate whether to show visual separator between sections, e.g., hr element use_section_visual_separator: bool = False # Indicate whether TN book intros should be included. Currently, # the content team does not want them included. diff --git a/frontend/src/lib/stores/SettingsStore.ts b/frontend/src/lib/stores/SettingsStore.ts index e8a02784..dc39c365 100644 --- a/frontend/src/lib/stores/SettingsStore.ts +++ b/frontend/src/lib/stores/SettingsStore.ts @@ -17,3 +17,4 @@ export let documentRequestKeyStore: Writable = writable('') export let settingsUpdated: Writable = writable(false) export let useChapterLabelsStore: Writable = writable(false) export let useSectionVisualSeparatorStore: Writable = writable(false) +export let usePrinceStore: Writable = writable(false) diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index df091e0b..31c95da4 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -13,7 +13,8 @@ documentRequestKeyStore, settingsUpdated, useChapterLabelsStore, - useSectionVisualSeparatorStore + useSectionVisualSeparatorStore, + usePrinceStore } from '$lib/stores/SettingsStore' import { documentReadyStore, errorStore } from '$lib/stores/NotificationStore' import { @@ -160,6 +161,19 @@ PDF + {#if $docTypeStore === 'pdf'} +
+ + Use PrinceXml to produce the PDF (much faster and better quality, but with Prince's 'P' logo at top + right of first page of PDF) +
+ {/if} {/if}

Layout

diff --git a/frontend/src/routes/settings/GenerateDocument.svelte b/frontend/src/routes/settings/GenerateDocument.svelte index 27a8117e..822a7733 100644 --- a/frontend/src/routes/settings/GenerateDocument.svelte +++ b/frontend/src/routes/settings/GenerateDocument.svelte @@ -20,7 +20,8 @@ documentRequestKeyStore, settingsUpdated, useChapterLabelsStore, - useSectionVisualSeparatorStore + useSectionVisualSeparatorStore, + usePrinceStore } from '$lib/stores/SettingsStore' import { taskStateStore } from '$lib/stores/TaskStore' import { getCode, getResourceTypeLangCode, getResourceTypeCode } from '$lib/utils' @@ -88,7 +89,8 @@ document_request_source: 'ui', limit_words: $limitTwStore, use_chapter_labels: $useChapterLabelsStore, - use_section_visual_separator: $useSectionVisualSeparatorStore + use_section_visual_separator: $useSectionVisualSeparatorStore, + use_prince: $usePrinceStore } console.log('document request: ', JSON.stringify(documentRequest, null, 2)) $errorStore = null diff --git a/frontend/tests/e2e/passages_test.ts b/frontend/tests/e2e/passages_test.ts index 1ef8dafa..e722653e 100644 --- a/frontend/tests/e2e/passages_test.ts +++ b/frontend/tests/e2e/passages_test.ts @@ -1,63 +1,69 @@ import { test, expect } from '@playwright/test' -test('test', async ({ page }) => { - await page.goto('http://localhost:8001/passages') - await page.getByText('Español Latin America (Latin').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByLabel('Bible Book').selectOption('jos') - await page.getByLabel('Chapter').selectOption('7') - await page.getByPlaceholder('e.g., 1,2,5-').click() - await page.getByPlaceholder('e.g., 1,2,5-').fill('1-10') - await page.getByRole('button', { name: 'Add Passage' }).click() - await page.getByLabel('Bible Book').selectOption('est') - await page.getByLabel('Chapter').selectOption('7') - await page.getByPlaceholder('e.g., 1,2,5-').click() - await page.getByPlaceholder('e.g., 1,2,5-').fill('3,12') - await page.getByRole('button', { name: 'Add Passage' }).click() - await page.getByLabel('Bible Book').selectOption('1th') - await page.getByLabel('Chapter').selectOption('4') - await page.getByPlaceholder('e.g., 1,2,5-').click() - await page.getByPlaceholder('e.g., 1,2,5-').fill('1,5-8,11') - await page.getByRole('button', { name: 'Add Passage' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Generate File' }).click() +test('test add passages', async ({ page }) => { + await page.goto('http://localhost:8001/passages') + await page.getByText('Español Latin America (Latin').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByLabel('Bible Book').selectOption('jos') + await page.getByLabel('Chapter').selectOption('7') + await page.getByPlaceholder('e.g., 1,2,5-').click() + await page.getByPlaceholder('e.g., 1,2,5-').fill('1-10') + await page.getByRole('button', { name: 'Add Passage' }).click() + await page.getByLabel('Bible Book').selectOption('est') + await page.getByLabel('Chapter').selectOption('7') + await page.getByPlaceholder('e.g., 1,2,5-').click() + await page.getByPlaceholder('e.g., 1,2,5-').fill('3,12') + await page.getByRole('button', { name: 'Add Passage' }).click() + await page.getByLabel('Bible Book').selectOption('1th') + await page.getByLabel('Chapter').selectOption('4') + await page.getByPlaceholder('e.g., 1,2,5-').click() + await page.getByPlaceholder('e.g., 1,2,5-').fill('1,5-8,11') + await page.getByRole('button', { name: 'Add Passage' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Generate File' }).click() }) + test('test add nt survey reviewer passages', async ({ page }) => { - await page.goto('http://localhost:8001/passages') - await page.getByRole('button', { name: 'Heart' }).click() - await page.getByText('Abure').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('link', { name: 'Language' }).click() - await page.getByRole('button', { name: 'Gateway' }).click() - await page.getByText('Bahasa Indonesia (Indonesian)').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByLabel('Add NT Survey Reviewers\'').check() - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.getByText('Matius 2:1-')).toBeVisible() + await page.goto('http://localhost:8001/passages') + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByText('Abure').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('link', { name: 'Language' }).click() + await page.getByRole('button', { name: 'Gateway' }).click() + await page.getByText('Bahasa Indonesia (Indonesian)').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText("Add NT Survey Reviewers'").click() + await page.getByRole('button', { name: 'Next' }).click() + await expect(page.locator('body')).toContainText('Matius 2:1-12') + // await expect(page.getByText('Matius 2:1-12')).toBeVisible() }) -test('test that you can select gateway tab after first selecting heart language and hitting next', async ({ page }) => { - await page.goto('http://localhost:8001/passages') - await page.getByRole('button', { name: 'Heart' }).click() - await page.getByText('Abure').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('link', { name: 'Language' }).click() - await page.getByRole('button', { name: 'Gateway' }).click() - await page.getByText('Bahasa Indonesia (Indonesian)').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByLabel('Add NT Survey Reviewers\'').check() - await expect(page.getByText('Matius 2:1-')).toBeVisible() - await page.getByRole('button', { name: 'Next' }).click() - await expect(page.getByText('Matius 2:1-')).toBeVisible() +test('test that you can select gateway tab after first selecting heart language and hitting next', async ({ + page +}) => { + await page.goto('http://localhost:8001/passages') + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByText('Abure').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('link', { name: 'Language' }).click() + await page.getByRole('button', { name: 'Gateway' }).click() + await page.getByText('Bahasa Indonesia (Indonesian)').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByLabel("Add NT Survey Reviewers'").check() + // await expect(page.getByText('Matius 2:1-')).toBeVisible() + await expect(page.locator('body')).toContainText('Matius 2:1-12') + await page.getByRole('button', { name: 'Next' }).click() + // await expect(page.getByText('Matius 2:1-')).toBeVisible() + await expect(page.locator('body')).toContainText('Matius 2:1-12') }) test('test add stet verse list to passages', async ({ page }) => { - await page.goto('http://localhost:8001/passages') - await page.getByText('Cebuano').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Add STET Passages').click() - await page.getByText('Mateo 1:1', { exact: true }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Generate File' }).click() + await page.goto('http://localhost:8001/passages') + await page.getByText('Cebuano').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Add STET Passages').click() + await page.getByText('Mateo 1:1', { exact: true }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Generate File' }).click() }) diff --git a/frontend/tests/e2e/test.ts b/frontend/tests/e2e/test.ts index d0e057f2..74167752 100644 --- a/frontend/tests/e2e/test.ts +++ b/frontend/tests/e2e/test.ts @@ -136,7 +136,9 @@ test('test that reviewers guide is only shown when book is chosen that it includ await page.getByRole('button', { name: 'Next' }).click() await expect( page.locator('span').filter({ hasText: "NT Survey Reviewers' Guide" }) - ).toBeVisible({ timeout: 5800000 }) + ).toBeVisible({ + timeout: 5800000 + }) }) test('test that you can select gateway tab after first selecting heart language and hitting next', async ({ @@ -254,8 +256,8 @@ test('test ordering of books in document title(s) and body', async ({ page }) => await page.getByText('Mak').click() await page.getByText('Luk', { exact: true }).click() await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Unlocked Literal Bible').click() - await page.getByText('Regular').click() + await page.getByText('Unlocked Literal Bible').first().click() + await page.getByText('Unlocked Literal Bible').nth(1).click() await page.getByRole('button', { name: 'Next' }).click() await page.getByText('PDF').click() await page.getByText('Interleave content by chapter').click() @@ -271,7 +273,7 @@ test('test ordering of books in document title(s) and body', async ({ page }) => const page1 = await page1Promise // Perform text expectations on the popup page await expect(page1.locator('body')).toContainText( - 'Ontenu (Ontenu): Regular for Matthew, Maki, Luk' + 'Ontenu (Ontenu): Unlocked Literal Bible for Matthew, Maki, Luk' ) await expect(page1.locator('body')).toContainText( 'Tok Pisin (Tok Pisin): Unlocked Literal Bible for Matyu, Mak, Luk' @@ -312,3 +314,27 @@ test('test ordering of books in document title(s) and body', async ({ page }) => expect(index3).toBeLessThan(index5) expect(index4).toBeLessThan(index5) }) + +test('test use prince with lots of books', async ({ page }) => { + await page.goto('http://localhost:8001/') + await page.getByPlaceholder('Search Gateway Languages').click() + await page.getByPlaceholder('Search Gateway Languages').fill('tpi') + await page.getByText('Tok Pisin').click() + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByPlaceholder('Search Heart Languages').click() + await page.getByPlaceholder('Search Heart Languages').fill('ont') + await page.getByText('Ontenu').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Select all').click() + await page.getByRole('button', { name: 'Old Testament' }).click() + await page.getByRole('button', { name: 'New Testament' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Unlocked Literal Bible').first().click() + await page.getByText('Unlocked Literal Bible').nth(1).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('PDF').click() + await page.getByText('Interleave content by chapter').click() + await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() + await page.getByText('Use PrinceXml to produce the').click() + await page.getByRole('button', { name: 'Generate File' }).click() +}) diff --git a/tests/e2e/doc/test_use_prince.py b/tests/e2e/doc/test_use_prince.py new file mode 100644 index 00000000..57282e9c --- /dev/null +++ b/tests/e2e/doc/test_use_prince.py @@ -0,0 +1,39 @@ +from doc.config import settings +from doc.domain import model +from doc.entrypoints.app import app +from fastapi.testclient import TestClient + +from tests.shared.utils import check_finished_document_with_verses_success + + +logger = settings.logger(__name__) + + +def test_en_ulb_tn_jud_language_book_order_1c_use_prince() -> None: + with TestClient(app=app, base_url=settings.api_test_url()) as client: + response = client.post( + "/documents", + json={ + "email_address": settings.TO_EMAIL_ADDRESS, + "assembly_strategy_kind": model.AssemblyStrategyEnum.LANGUAGE_BOOK_ORDER, + "assembly_layout_kind": model.AssemblyLayoutEnum.ONE_COLUMN, + "layout_for_print": False, + "generate_pdf": True, + "generate_epub": False, + "generate_docx": False, + "use_prince": True, + "resource_requests": [ + { + "lang_code": "en", + "resource_type": "ulb", + "book_code": "jud", + }, + { + "lang_code": "en", + "resource_type": "tn", + "book_code": "jud", + }, + ], + }, + ) + check_finished_document_with_verses_success(response, suffix="pdf") From ef834af9b439390307e7517bc54ae57fa1a3d34e Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 31 Jul 2025 14:18:23 -0700 Subject: [PATCH 129/208] Remove unused constants --- backend/doc/domain/resource_lookup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index c6aa264a..19a89047 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -51,9 +51,7 @@ logger = settings.logger(__name__) -SOURCE_DATA_JSON_FILENAME = "resources.json" -SOURCE_GATEWAY_LANGUAGES_FILENAME = "gateway_languages.json" # This can be expanded to include any additional types (if # there are any) that we want to be available to users. These are all From f707be32bfb6c4f72544828b9946831809e58e34 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 31 Jul 2025 14:20:39 -0700 Subject: [PATCH 130/208] Handle validation error exception --- backend/doc/domain/resource_lookup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 19a89047..13ef6f57 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -47,7 +47,7 @@ from doc.utils.list_utils import unique_tuples, unique_book_codes from doc.utils.text_utils import normalize_localized_book_name from fastapi import HTTPException, status -from pydantic import HttpUrl +from pydantic import HttpUrl, ValidationError logger = settings.logger(__name__) @@ -251,6 +251,14 @@ def fetch_source_data( logger.exception("Request failed: %s", e) logger.info("Failed to get data from data API, API might be down...") return SourceData(git_repo=[]) + except ValidationError as e: + logger.exception( + "Request failed due to invalid data returned from data API: %s", e + ) + logger.info( + "Some of the data returned by data API is invalid, check logs for details" + ) + return SourceData(git_repo=[]) def lang_codes_and_names( From cd51792b7a7b888dc1121ed6eb1c75c5e7f6a997 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 31 Jul 2025 14:21:31 -0700 Subject: [PATCH 131/208] Simplify batch_clone_git_repos function --- backend/doc/domain/resource_lookup.py | 62 +++++++++++++-------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 13ef6f57..c11537e1 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -517,41 +517,41 @@ def batch_clone_git_repos( ) -> None: """ Clones multiple git repositories in a single batch operation. - - If a repository already exists and is fully cloned, it is skipped. - - If a repository exists but is a partial clone (corrupt or missing key files), it is removed first. - - If asset_caching_enabled is False, repositories are always deleted and re-cloned. + - If a repository already exists, is fully cloned, and not stale (with respect to cache period), it is skipped. + Conversely, if a repository is fully cloned, but stale then it is removed before (re)cloning. + - If a repository exists but is a partial clone (corrupt or missing key files), it is removed before (re)cloning. + - If asset_caching_enabled is False, repositories are always removed and re-cloned. """ clone_commands = [] for url, resource_filepath in repos: + if asset_caching_enabled: + try: + git_dir = join(resource_filepath, ".git") + stat_ = stat(git_dir) + mod_time = datetime.fromtimestamp(stat_.st_mtime) + expiry = timedelta(minutes=asset_caching_period) + if ( + all( + exists(join(git_dir, filename)) + for filename in ["config", "HEAD", "objects"] + ) + and any(scandir(resource_filepath)) + and datetime.now() - mod_time <= expiry + ): + logger.info( + f"Skipping clone: {resource_filepath} already exists, is a valid git repo, and is not stale." + ) + continue # ✅ Fully cloned and not stale, reuse + except FileNotFoundError: + logger.warning(f"Git directory, {git_dir}, not found") + logger.info( + f"Removing stale, incomplete, or corrupt repository: {resource_filepath}" + ) + else: + logger.info( + f"Asset caching disabled: forcibly removing {resource_filepath}" + ) if isdir(resource_filepath): - git_dir = join(resource_filepath, ".git") - if asset_caching_enabled: - if isdir(git_dir): - try: - stat_ = stat(git_dir) - mod_time = datetime.fromtimestamp(stat_.st_mtime) - expiry = timedelta(minutes=asset_caching_period) - if ( - all( - exists(join(git_dir, filename)) - for filename in ["config", "HEAD", "objects"] - ) - and datetime.now() - mod_time <= expiry - and any(scandir(resource_filepath)) - ): - logger.info( - f"Skipping clone: {resource_filepath} already exists and is a full repo." - ) - continue # ✅ Fully cloned, reuse - except FileNotFoundError: - logger.warning(f"Git directory not found: {git_dir}") - logger.info( - f"Removing stale, incomplete, or corrupt repository: {resource_filepath}" - ) - else: - logger.info( - f"Asset caching disabled: forcibly removing {resource_filepath}" - ) shutil.rmtree(resource_filepath) clone_command = ( f"git -c http.userAgent='{user_agent_str}' " From 2525d5adb4ec7c63be2d91c9d79f70b7a9d847cd Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 31 Jul 2025 14:22:03 -0700 Subject: [PATCH 132/208] Update doctest result --- backend/doc/domain/resource_lookup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index c11537e1..e86b3724 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -1108,6 +1108,10 @@ def book_codes_for_lang( (...) >>> result[0] ('gen', 'Gênesis') + >>> (); result2 = resource_lookup.book_codes_for_lang("zh");() # doctest: +ELLIPSIS + (...) + >>> result2 + [('gen', 'Genesis'), ('exo', 'Exodus'), ('lev', 'Leviticus'), ('num', 'Numbers'), ('deu', 'Deuteronomy'), ('jos', 'Joshua'), ('jdg', 'Judges'), ('rut', 'Ruth'), ('1sa', '1 Samuel'), ('2sa', '2 Samuel'), ('1ki', '1 Kings'), ('2ki', '2 Kings'), ('1ch', '1 Chronicles'), ('2ch', '2 Chronicles'), ('ezr', 'Ezra'), ('neh', 'Nehemiah'), ('est', 'Esther'), ('job', 'Job'), ('psa', 'Psalms'), ('pro', 'Proverbs'), ('ecc', 'Ecclesiastes'), ('sng', 'Song of Solomon'), ('isa', 'Isaiah'), ('jer', 'Jeremiah'), ('lam', 'Lamentations'), ('ezk', 'Ezekiel'), ('dan', 'Daniel'), ('hos', 'Hosea'), ('jol', 'Joel'), ('amo', 'Amos'), ('oba', 'Obadiah'), ('jon', 'Jonah'), ('mic', 'Micah'), ('nam', 'Nahum'), ('hab', 'Habakkuk'), ('zep', 'Zephaniah'), ('hag', 'Haggai'), ('zec', 'Zechariah'), ('mal', 'Malachi'), ('mat', 'Matthew'), ('mrk', 'Mark'), ('luk', 'Luke'), ('jhn', 'John'), ('act', 'Acts'), ('rom', 'Romans'), ('1co', '1 Corinthians'), ('2co', '2 Corinthians'), ('gal', 'Galatians'), ('eph', 'Ephesians'), ('php', 'Philippians'), ('col', 'Colossians'), ('1th', '1 Thessalonians'), ('2th', '2 Thessalonians'), ('1ti', '1 Timothy'), ('2ti', '2 Timothy'), ('tit', 'Titus'), ('phm', 'Philemon'), ('heb', 'Hebrews'), ('jas', 'James'), ('1pe', '1 Peter'), ('2pe', '2 Peter'), ('1jn', '1 John'), ('2jn', '2 John'), ('3jn', '3 John'), ('jud', 'Jude'), ('rev', 'Revelation')] """ return get_book_codes_for_lang( lang_code, From 9762704821954ecac6e02f59fac42e842c806dd1 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 31 Jul 2025 14:22:21 -0700 Subject: [PATCH 133/208] Make sure to declare variable for case of failure --- backend/doc/domain/resource_lookup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index e86b3724..93280f37 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -913,6 +913,7 @@ def get_book_codes_for_lang( if data is None: return [] repo_clone_list: list[tuple[HttpUrl, str]] = [] + book_codes_and_names: list[tuple[str, str]] = [] try: repos_info = data.git_repo augmented_repos_info = add_data_not_supplied_by_data_api(repos_info) From a71851668f5cac5e1e4dd09cf39a4912c1f6125f Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 31 Jul 2025 14:23:20 -0700 Subject: [PATCH 134/208] Gracefully handle case where data API returns null resource_type This is a questionable change because it means that we don't catch invalid data in the data API as quickly for the case when content.resource_type is null (which is an invalid state). However, without this commit, if the data API returns content.resource_type null ever again, it zombies this app and others that depend on it as an API which is bad for users. After this commit this type of error will only be detected by a DOC user if they were expecting to see the offending record and it isn't present in the results. --- backend/doc/domain/resource_lookup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 93280f37..b48adb6d 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -238,7 +238,13 @@ def fetch_source_data( if response.status_code == 200: data_payload = response.json().get("data", {}) if "git_repo" in data_payload: - return SourceData.model_validate(data_payload) + # return SourceData.model_validate(data_payload) + valid_repos = [ + repo + for repo in data_payload["git_repo"] + if repo.get("content", {}).get("resource_type") is not None + ] + return SourceData.model_validate({"git_repo": valid_repos}) else: logger.info("Invalid payload structure, no data.") return SourceData(git_repo=[]) From 183e05ea0de0652b26f8f0b7d2348d145a78ddd7 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 31 Jul 2025 14:26:43 -0700 Subject: [PATCH 135/208] Cache returns from our oft used but singular query to data API This really speeds up DOC (and thus STET, and Passages apps). The cache duration is set to 3 minutes to approximately cover one user interaction span which greatly improves the perceived speed of the app(s). --- backend/doc/domain/resource_lookup.py | 3 +++ backend/requirements.in | 2 ++ backend/requirements.txt | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index b48adb6d..bce41834 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -4,6 +4,7 @@ assets. """ +from cachetools import TTLCache, cached from datetime import datetime, timedelta import json import re @@ -51,6 +52,7 @@ logger = settings.logger(__name__) +fetch_source_data_cache: TTLCache[str, SourceData] = TTLCache(maxsize=1, ttl=180) # This can be expanded to include any additional types (if @@ -199,6 +201,7 @@ X_REQUESTED_WITH_VALUE: str = "WA-Tool-Doc" +@cached(fetch_source_data_cache) def fetch_source_data( data_api_url: HttpUrl = settings.DATA_API_URL, user_agent_str: str = USER_AGENT_STR, diff --git a/backend/requirements.in b/backend/requirements.in index 6c41b4ec..43b0dcbf 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -5,6 +5,7 @@ # cython # For pydantic: https://pydantic-docs.helpmanual.io/install/ # TODO do we still need aiofiles? aiofiles +cachetools celery celery-types docxtpl @@ -29,6 +30,7 @@ termcolor uvicorn weasyprint +types-cachetools types-PyYAML types-beautifulsoup4 types-orjson diff --git a/backend/requirements.txt b/backend/requirements.txt index be62231a..310e3692 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -23,6 +23,8 @@ billiard==4.2.1 # via celery brotli==1.1.0 # via fonttools +cachetools==6.1.0 + # via -r ./backend/requirements.in celery==5.4.0 # via # -r ./backend/requirements.in @@ -204,6 +206,8 @@ typer==0.13.1 # via fastapi-cli types-beautifulsoup4==4.12.0.20241020 # via -r ./backend/requirements.in +types-cachetools==6.1.0.20250717 + # via -r ./backend/requirements.in types-html5lib==1.1.11.20241018 # via types-beautifulsoup4 types-orjson==3.6.2 From b15e61b35e7fe64b94bbd68c1f8d63c8170270db Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 31 Jul 2025 14:28:22 -0700 Subject: [PATCH 136/208] Small refactor Self documenting --- backend/doc/entrypoints/app.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/doc/entrypoints/app.py b/backend/doc/entrypoints/app.py index 5d243ed1..da3d49a9 100644 --- a/backend/doc/entrypoints/app.py +++ b/backend/doc/entrypoints/app.py @@ -82,10 +82,7 @@ async def validation_exception_handler( ) -# Until reviewer's guides can be accessed via data API, create their -# directory and copy them into place -@app.on_event("startup") -async def initialize_assets() -> None: +def initialize_assets() -> None: """ Ensures the en_rg directory and the .docx file exist in the assets_download volume. """ @@ -95,7 +92,9 @@ async def initialize_assets() -> None: if not exists(DOCKER_DOCX_FILE_DEST): shutil.copy(DOCKER_DOCX_FILE_SRC, DOCKER_DOCX_FILE_DEST) if not exists(DOCKER_DOCX_FILE_DEST): - raise AssertionError("en_rg_nt_survey.docx not copied into place at startup!") + raise AssertionError( + "en_rg_nt_survey.docx not copied into place at startup!" + ) elif exists(LOCAL_ASSETS_DOWNLOAD_DIR): # Executing outside Docker container makedirs(LOCAL_EN_RG_DIR, exist_ok=True) if not exists(LOCAL_DOCX_FILE_DEST): @@ -105,6 +104,13 @@ async def initialize_assets() -> None: print(f"Error initializing assets: {e}") +# Until reviewer's guides can be accessed via data API, create their +# directory and copy them into place +@app.on_event("startup") +async def startup() -> None: + initialize_assets() + + app.include_router(doc_router) app.include_router(stet_router) app.include_router(passages_router) From 1eb2d3b529e4624bb9e589eecf6b8cb61cc78388 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 5 Aug 2025 14:58:25 -0700 Subject: [PATCH 137/208] Update source code comments --- backend/doc/domain/document_generator.py | 11 ----------- backend/doc/domain/resource_lookup.py | 10 ++++------ backend/doc/utils/tw_utils.py | 3 +-- frontend/src/routes/settings/+page.svelte | 1 - tests/e2e/conftest.py | 2 +- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index 5e80c4a4..ccc90b94 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -107,12 +107,7 @@ def locate_acquire_and_build_resource_objects( Sequence[BCBook], Sequence[RGBook], ]: - # Update the state of the worker process. This is used by the - # UI to report status. current_task.update_state(state="Locating assets") - # Docx didn't exist in cache so go ahead and start by getting the - # resource lookup DTOs for each resource request in the document - # request. resource_lookup_dtos = [] for resource_request in document_request.resource_requests: resource_lookup_dto = resource_lookup.resource_lookup_dto( @@ -122,16 +117,11 @@ def locate_acquire_and_build_resource_objects( ) if resource_lookup_dto: resource_lookup_dtos.append(resource_lookup_dto) - # Determine which resource URLs were actually found. found_resource_lookup_dtos = [ resource_lookup_dto for resource_lookup_dto in resource_lookup_dtos if resource_lookup_dto.url is not None ] - # if not found_resource_lookup_dtos: - # raise exceptions.ResourceAssetFileNotFoundError( - # message="No supported resource assets were found" - # ) current_task.update_state(state="Provisioning asset files") t0 = time.time() resource_dirs = [ @@ -145,7 +135,6 @@ def locate_acquire_and_build_resource_objects( "Time to provision asset files (acquire and write to disk): %s", t1 - t0 ) current_task.update_state(state="Parsing asset files") - # Initialize found resources from their provisioned assets. t0 = time.time() usfm_books, tn_books, tq_books, tw_books, bc_books, rg_books = parsing.books( found_resource_lookup_dtos, diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index bce41834..d2942db8 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -77,7 +77,7 @@ "ulb": "Unlocked Literal Bible", } -# NOTE This is only used to see if a lang_code is in the collection +# This is only used to see if a lang_code is in the collection # otherwise it is a heart language. Eventually the graphql data api may # provide gateway/heart boolean value. GATEWAY_LANGUAGES: Sequence[str] = [ @@ -179,10 +179,6 @@ "zlm", ] -# The book name in the tuple key is what -# resource_lookup.get_book_codes_for_lang is returning for lang_code in -# the tuple key and the associated value is what we would prefer to -# use. BOOK_NAME_CORRECTION_TABLE: dict[tuple[str, str], str] = { ("pt-br", "1 Corintios"): "1 Coríntios", ("es-419", "I juan"): "1 Juan", @@ -461,6 +457,8 @@ def resource_types( return sorted(unique_values, key=lambda value: value[1]) +# We found that there are fewer downloads available than clonable repos, +# so we don't currently have the system configured to use this. def batch_download_repos( repos: list[tuple[HttpUrl, str]], asset_caching_enabled: bool = settings.ASSET_CACHING_ENABLED, @@ -675,7 +673,7 @@ def get_last_segment(url: HttpUrl, lang_code: str) -> str: ("zmq", "faustin_azaza"): "zmq_mrk_text_reg", } -# Prefixes to remove regardless of lang code +# Prefixes to remove in last repo URL segment regardless of lang code PREFIXES_TO_REMOVE = [ "Dawit-Dessie_", "Jordan_", diff --git a/backend/doc/utils/tw_utils.py b/backend/doc/utils/tw_utils.py index 0060669c..7e7997d4 100644 --- a/backend/doc/utils/tw_utils.py +++ b/backend/doc/utils/tw_utils.py @@ -123,9 +123,8 @@ def translation_words_section( the list of all translation words for this language, book combination. Limit the translation words to only those that appear in the USFM resouce chosen if limit_words is True and a USFM resource was also - chosen. + chosen otherwise include all the translation words for the language. """ - content = [] if tw_book.name_content_pairs: content.append(resource_type_name_fmt_str.format(tw_book.resource_type_name)) diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 31c95da4..ecbce21c 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -108,7 +108,6 @@ -

File type

diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 75ab12da..93e7e8c7 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -55,7 +55,7 @@ def random_non_english_lang_code2() -> str: @pytest.fixture(params=bible_books.BOOK_NAMES.keys()) def book_code(request: Any) -> Any: - """All book names sequentially, but one at a time.""" + """All book codes sequentially, but one at a time.""" return request.param From 2d2419ee328a780e42b04571b80a4b45b84f74e7 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 1 Aug 2025 09:22:11 -0700 Subject: [PATCH 138/208] Fix bug where multiple copies of a translation word could appear --- backend/doc/domain/document_generator.py | 24 +++++++++++------------- backend/doc/utils/tw_utils.py | 5 ++++- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index ccc90b94..8fe709f0 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -484,20 +484,18 @@ def assemble_content( logger.info("Time for interleaving document: %s", t1 - t0) t0 = time.time() # Add the translation words definition section for each language requested. - unique_lang_codes = set() - for tw_book in tw_books: - if tw_book.lang_code not in unique_lang_codes: - unique_lang_codes.add(tw_book.lang_code) - content.extend( - translation_words_section( - tw_book, - usfm_books, - document_request.limit_words, - document_request.resource_requests, - ) + unique_tw_books = filter_unique_by_lang_code(tw_books) + for tw_book in unique_tw_books: + content.extend( + translation_words_section( + tw_book, + usfm_books, + document_request.limit_words, + document_request.resource_requests, ) - if document_request.use_section_visual_separator: - content.append(hr) + ) + if document_request.use_section_visual_separator: + content.append(hr) t1 = time.time() logger.info("Time for add TW content to document: %s", t1 - t0) return content diff --git a/backend/doc/utils/tw_utils.py b/backend/doc/utils/tw_utils.py index 7e7997d4..cd1afdf5 100644 --- a/backend/doc/utils/tw_utils.py +++ b/backend/doc/utils/tw_utils.py @@ -158,6 +158,7 @@ def filter_name_content_pairs( tw_book: TWBook, usfm_books: Optional[Sequence[USFMBook]] ) -> list[TWNameContentPair]: selected_name_content_pairs = [] + added_pairs = set() if usfm_books: for name_content_pair in tw_book.name_content_pairs: for usfm_book in usfm_books: @@ -166,7 +167,9 @@ def filter_name_content_pairs( re.escape(name_content_pair.localized_word), chapter.content, ): - selected_name_content_pairs.append(name_content_pair) + if name_content_pair not in added_pairs: + selected_name_content_pairs.append(name_content_pair) + added_pairs.add(name_content_pair) break return selected_name_content_pairs From 9c11e4d89408b8d0f8d32055b7978daa5f768238 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Sat, 2 Aug 2025 15:22:44 -0700 Subject: [PATCH 139/208] Add test that ensures ordering of USFM content Forgot to commit this an update or two ago while back --- tests/unit/test_assemble_docx_content.py | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/unit/test_assemble_docx_content.py diff --git a/tests/unit/test_assemble_docx_content.py b/tests/unit/test_assemble_docx_content.py new file mode 100644 index 00000000..ec1d6c8f --- /dev/null +++ b/tests/unit/test_assemble_docx_content.py @@ -0,0 +1,68 @@ +import pytest +from doc.domain import document_generator, parsing, resource_lookup + + +def test_assemble_docx_content_ordering_of_books() -> None: + document_request_json = '{"email_address":null,"assembly_strategy_kind":"lbo","assembly_layout_kind":"1c","layout_for_print":false,"resource_requests":[{"lang_code":"tpi","resource_type":"ulb","book_code":"mat"}, {"lang_code":"tpi","resource_type":"ulb","book_code":"mrk"},{"lang_code":"tpi","resource_type":"ulb","book_code":"luk"}],"generate_pdf":true,"generate_epub":false,"generate_docx":false,"chunk_size":"chapter","limit_words":false,"include_tn_book_intros":false,"document_request_source":"ui"}' + document_request, document_request_key = ( + document_generator.initialize_document_request_and_key(document_request_json) + ) + resource_lookup_dtos = [] + for resource_request in document_request.resource_requests: + resource_lookup_dto = resource_lookup.resource_lookup_dto( + resource_request.lang_code, + resource_request.resource_type, + resource_request.book_code, + ) + if resource_lookup_dto: + resource_lookup_dtos.append(resource_lookup_dto) + found_resource_lookup_dtos = [ + resource_lookup_dto + for resource_lookup_dto in resource_lookup_dtos + if resource_lookup_dto.url is not None + ] + resource_dirs = [ + resource_lookup.prepare_resource_filepath(dto) + for dto in found_resource_lookup_dtos + ] + for resource_dir, dto in zip(resource_dirs, found_resource_lookup_dtos): + resource_lookup.provision_asset_files(dto.url, resource_dir) + usfm_books, tn_books, tq_books, tw_books, bc_books, rg_books = parsing.books( + found_resource_lookup_dtos, + resource_dirs, + document_request.resource_requests, + document_request.layout_for_print, + document_request.use_chapter_labels, + ) + document_parts = document_generator.assemble_docx_content( + document_request_key, + document_request, + usfm_books, + tn_books, + tq_books, + tw_books, + bc_books, + rg_books, + ) + + def contains_before(seq: list[str], a: str, b: str) -> bool: + a_index = b_index = None + for i, s in enumerate(seq): + if a_index is None and a in s: + a_index = i + if b_index is None and b in s: + b_index = i + if a_index is not None and b_index is not None: + break + if a_index is None or b_index is None: + return False # One or both substrings not found + return a_index < b_index + + content = [part.content for part in document_parts] + assert contains_before(content, "Matyu", "Mak") + assert contains_before(content, "Mak", "Luk") + assert contains_before(content, "Matyu", "Luk") + + +if __name__ == "__main__": + pytest.main() From c2b3f883688c72df4d64ef6e684a334ae596dc1d Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 4 Aug 2025 09:00:32 -0700 Subject: [PATCH 140/208] Move shared constant to Settings --- backend/doc/config.py | 2 ++ .../assembly_strategies_book_then_lang_by_chapter.py | 5 ++--- .../assembly_strategies_lang_then_book_by_chapter.py | 3 +-- .../assembly_strategies_book_then_lang_by_chapter.py | 4 +--- .../assembly_strategies_lang_then_book_by_chapter.py | 4 +--- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/backend/doc/config.py b/backend/doc/config.py index c02a5366..5cdddd6e 100755 --- a/backend/doc/config.py +++ b/backend/doc/config.py @@ -41,6 +41,8 @@ class Settings(BaseSettings): USE_LOCALIZED_BOOK_NAME: bool CHECK_ALL_BOOKS_FOR_LANGUAGE: bool + BOOK_NAME_FMT_STR: str = "

{}

" + DOWNLOAD_ASSETS: bool # If true then download assets, else clone assets def logger(self, name: str) -> logging.Logger: diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py index 0aa391a6..8e422fa4 100644 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py @@ -38,7 +38,6 @@ HTML_COLUMN_LEFT_BEGIN: str = "
" HTML_COLUMN_RIGHT_BEGIN: str = "
" END_OF_CHAPTER_HTML: str = '
' -BOOK_NAME_FMT_STR: str = "

{}

" def assemble_content_by_book_then_lang( @@ -200,7 +199,7 @@ def assemble_usfm_by_chapter( hr: str = "
", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, - fmt_str: str = BOOK_NAME_FMT_STR, + fmt_str: str = settings.BOOK_NAME_FMT_STR, ) -> list[str]: """ Construct the HTML wherein at least one USFM resource exists, one column @@ -542,7 +541,7 @@ def assemble_usfm_by_chapter_2c_sl_sr( html_row_end: str = HTML_ROW_END, close_direction_html: str = "
", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, - fmt_str: str = BOOK_NAME_FMT_STR, + fmt_str: str = settings.BOOK_NAME_FMT_STR, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, ) -> list[str]: """ diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py index 323360f8..f21f5b7b 100755 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py @@ -30,7 +30,6 @@ logger = settings.logger(__name__) END_OF_CHAPTER_HTML: str = '
' -BOOK_NAME_FMT_STR: str = "

{}

" def assemble_content_by_lang_then_book( @@ -187,7 +186,7 @@ def assemble_usfm_by_book( end_of_chapter_html: str = END_OF_CHAPTER_HTML, hr: str = "
", close_direction_html: str = "
", - fmt_str: str = BOOK_NAME_FMT_STR, + fmt_str: str = settings.BOOK_NAME_FMT_STR, ) -> list[str]: content = [] content.append(usfm_language_direction_html(usfm_book)) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py index 7a8b092d..49a025c7 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py @@ -27,8 +27,6 @@ logger = settings.logger(__name__) -BOOK_NAME_FMT_STR: str = "

{}

" - def assemble_content_by_book_then_lang( usfm_books: Sequence[USFMBook], @@ -148,7 +146,7 @@ def assemble_usfm_by_chapter( use_section_visual_separator: bool, book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, - fmt_str: str = BOOK_NAME_FMT_STR, + fmt_str: str = settings.BOOK_NAME_FMT_STR, ) -> list[DocumentPart]: """ Construct the Docx wherein at least one USFM resource exists, one column diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py index 09164316..7c3a58fa 100755 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py @@ -27,8 +27,6 @@ logger = settings.logger(__name__) -BOOK_NAME_FMT_STR: str = "

{}

" - def assemble_content_by_lang_then_book( usfm_books: Sequence[USFMBook], @@ -189,7 +187,7 @@ def assemble_usfm_by_book( rg_book: Optional[RGBook], use_section_visual_separator: bool, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, - fmt_str: str = BOOK_NAME_FMT_STR, + fmt_str: str = settings.BOOK_NAME_FMT_STR, ) -> list[DocumentPart]: """ Construct the HTML for a 'by book' strategy wherein at least From fdf4d5521b642efea0108cc51e91d82c0f0bef0e Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 4 Aug 2025 10:03:30 -0700 Subject: [PATCH 141/208] Move shared constant to Settings --- backend/doc/config.py | 1 + .../assembly_strategies_book_then_lang_by_chapter.py | 9 ++++----- .../assembly_strategies_lang_then_book_by_chapter.py | 12 +++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/backend/doc/config.py b/backend/doc/config.py index 5cdddd6e..c920ea5b 100755 --- a/backend/doc/config.py +++ b/backend/doc/config.py @@ -42,6 +42,7 @@ class Settings(BaseSettings): CHECK_ALL_BOOKS_FOR_LANGUAGE: bool BOOK_NAME_FMT_STR: str = "

{}

" + END_OF_CHAPTER_HTML: str = '
' DOWNLOAD_ASSETS: bool # If true then download assets, else clone assets diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py index 8e422fa4..6d6b9ff9 100644 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py @@ -37,7 +37,6 @@ HTML_COLUMN_END: str = "
" HTML_COLUMN_LEFT_BEGIN: str = "
" HTML_COLUMN_RIGHT_BEGIN: str = "
" -END_OF_CHAPTER_HTML: str = '
' def assemble_content_by_book_then_lang( @@ -194,7 +193,7 @@ def assemble_usfm_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "
", hr: str = "
", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, @@ -327,7 +326,7 @@ def assemble_tn_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "
", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, @@ -430,7 +429,7 @@ def assemble_tq_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, ) -> list[str]: @@ -507,7 +506,7 @@ def assemble_tw_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, ) -> list[str]: content = [] diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py index f21f5b7b..31c1b7e7 100755 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py @@ -29,8 +29,6 @@ logger = settings.logger(__name__) -END_OF_CHAPTER_HTML: str = '
' - def assemble_content_by_lang_then_book( usfm_books: Sequence[USFMBook], @@ -183,7 +181,7 @@ def assemble_usfm_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, hr: str = "
", close_direction_html: str = "", fmt_str: str = settings.BOOK_NAME_FMT_STR, @@ -245,7 +243,7 @@ def assemble_tn_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> list[str]: content = [] @@ -283,7 +281,7 @@ def assemble_tq_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> list[str]: content = [] @@ -313,7 +311,7 @@ def assemble_rg_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> list[str]: """ @@ -373,7 +371,7 @@ def assemble_tw_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, - end_of_chapter_html: str = END_OF_CHAPTER_HTML, + end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> list[str]: content = [] From d2d71acbf68f6515bdd5315a458140ef2a9b22f1 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 4 Aug 2025 20:39:08 -0700 Subject: [PATCH 142/208] Give the user a choice of 1 or 2 col layout for TN and TQ Also add conditional display of settings based on resources user has chosen --- backend/doc/config.py | 1 + ...ly_strategies_book_then_lang_by_chapter.py | 53 +++++- ...ly_strategies_lang_then_book_by_chapter.py | 49 +++++- .../assembly_strategy_utils.py | 40 +++-- ...ly_strategies_book_then_lang_by_chapter.py | 47 +++-- ...ly_strategies_lang_then_book_by_chapter.py | 49 ++++-- .../assembly_strategy_utils.py | 26 ++- backend/doc/domain/document_generator.py | 16 +- backend/doc/domain/model.py | 5 + frontend/src/lib/OneColumnLayoutIcon.svelte | 18 ++ frontend/src/lib/TwoColumnLayoutIcon.svelte | 11 ++ frontend/src/lib/stores/SettingsStore.ts | 2 + frontend/src/routes/settings/+page.svelte | 165 +++++++++++++----- .../routes/settings/GenerateDocument.svelte | 8 +- frontend/tests/e2e/test.ts | 49 ++++++ tests/unit/test_document_generator.py | 2 + 16 files changed, 430 insertions(+), 111 deletions(-) create mode 100644 frontend/src/lib/OneColumnLayoutIcon.svelte create mode 100644 frontend/src/lib/TwoColumnLayoutIcon.svelte diff --git a/backend/doc/config.py b/backend/doc/config.py index c920ea5b..67bcb7d5 100755 --- a/backend/doc/config.py +++ b/backend/doc/config.py @@ -43,6 +43,7 @@ class Settings(BaseSettings): BOOK_NAME_FMT_STR: str = "

{}

" END_OF_CHAPTER_HTML: str = '
' + HR: str = "
" DOWNLOAD_ASSETS: bool # If true then download assets, else clone assets diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py index 6d6b9ff9..a765c936 100644 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_book_then_lang_by_chapter.py @@ -48,6 +48,8 @@ def assemble_content_by_book_then_lang( rg_books: Sequence[RGBook], assembly_layout_kind: AssemblyLayoutEnum, use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> str: @@ -103,6 +105,8 @@ def assemble_content_by_book_then_lang( selected_bc_books, selected_rg_books, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) elif ( @@ -122,6 +126,8 @@ def assemble_content_by_book_then_lang( selected_bc_books, selected_rg_books, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) elif ( @@ -142,6 +148,7 @@ def assemble_content_by_book_then_lang( selected_bc_books, selected_rg_books, use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) ) elif ( @@ -180,6 +187,8 @@ def assemble_content_by_book_then_lang( selected_bc_books, selected_rg_books, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) return "".join(content) @@ -193,9 +202,11 @@ def assemble_usfm_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", - hr: str = "
", + hr: str = settings.HR, book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, fmt_str: str = settings.BOOK_NAME_FMT_STR, @@ -280,7 +291,10 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tn_book.chapters: tn_verses = tn_chapter_verses( - tn_book, chapter_num, use_section_visual_separator + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, ) if tn_verses: content.append(tn_language_direction_html(tn_book)) @@ -292,7 +306,10 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tq_book.chapters: tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) if tq_verses: content.append(tq_language_direction_html(tq_book)) @@ -326,6 +343,8 @@ def assemble_tn_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, @@ -391,7 +410,10 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tn_book.chapters: tn_verses = tn_chapter_verses( - tn_book, chapter_num, use_section_visual_separator + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, ) content.append(tn_language_direction_html(tn_book)) content.append(tn_verses) @@ -401,7 +423,10 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tq_book.chapters: tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) content.append(tq_language_direction_html(tq_book)) content.append(tq_verses) @@ -429,6 +454,7 @@ def assemble_tq_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tq_notes: bool, end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", book_chapters: Mapping[str, int] = BOOK_CHAPTERS, @@ -470,7 +496,10 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tq_book.chapters: tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) if tq_verses: content.append(tq_language_direction_html(tq_book)) @@ -532,6 +561,8 @@ def assemble_usfm_by_chapter_2c_sl_sr( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, html_row_begin: str = HTML_ROW_BEGIN, html_column_begin: str = HTML_COLUMN_BEGIN, html_column_left_begin: str = HTML_COLUMN_LEFT_BEGIN, @@ -743,7 +774,10 @@ def rg_sort_key(resource: RGBook) -> str: tn_verses = None for idx, tn_book in enumerate(tn_books): tn_verses = tn_chapter_verses( - tn_book, chapter_num, use_section_visual_separator + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, ) if tn_verses: if is_even(idx): @@ -759,7 +793,10 @@ def rg_sort_key(resource: RGBook) -> str: tq_verses = None for idx, tq_book in enumerate(tq_books): tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) if tq_verses: if is_even(idx): diff --git a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py index 31c1b7e7..e88f9675 100755 --- a/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategies_lang_then_book_by_chapter.py @@ -39,6 +39,8 @@ def assemble_content_by_lang_then_book( rg_books: Sequence[RGBook], assembly_layout_kind: AssemblyLayoutEnum, use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> list[str]: @@ -123,6 +125,8 @@ def assemble_content_by_lang_then_book( bc_book, rg_book, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) elif usfm_book is None and tn_book is not None: @@ -136,6 +140,8 @@ def assemble_content_by_lang_then_book( bc_book, rg_book, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) elif usfm_book is None and tn_book is None and tq_book is not None: @@ -149,6 +155,7 @@ def assemble_content_by_lang_then_book( bc_book, rg_book, use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) ) elif ( @@ -181,8 +188,10 @@ def assemble_usfm_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, - hr: str = "
", + hr: str = settings.HR, close_direction_html: str = "", fmt_str: str = settings.BOOK_NAME_FMT_STR, ) -> list[str]: @@ -216,10 +225,20 @@ def assemble_usfm_by_book( chapter_commentary(bc_book, chapter_num, use_section_visual_separator) ) content.append( - tn_chapter_verses(tn_book, chapter_num, use_section_visual_separator) + tn_chapter_verses( + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, + ) ) content.append( - tq_chapter_verses(tq_book, chapter_num, use_section_visual_separator) + tq_chapter_verses( + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, + ) ) content.append( rg_chapter_verses(rg_book, chapter_num, use_section_visual_separator) @@ -243,6 +262,8 @@ def assemble_tn_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> list[str]: @@ -259,10 +280,20 @@ def assemble_tn_by_book( chapter_commentary(bc_book, chapter_num, use_section_visual_separator) ) content.append( - tn_chapter_verses(tn_book, chapter_num, use_section_visual_separator) + tn_chapter_verses( + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, + ) ) content.append( - tq_chapter_verses(tq_book, chapter_num, use_section_visual_separator) + tq_chapter_verses( + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, + ) ) content.append( rg_chapter_verses(rg_book, chapter_num, use_section_visual_separator) @@ -281,6 +312,7 @@ def assemble_tq_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tq_notes: bool, end_of_chapter_html: str = settings.END_OF_CHAPTER_HTML, close_direction_html: str = "", ) -> list[str]: @@ -293,7 +325,12 @@ def assemble_tq_by_book( ) content.append(chapter_heading(chapter_num)) content.append( - tq_chapter_verses(tq_book, chapter_num, use_section_visual_separator) + tq_chapter_verses( + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, + ) ) content.append( rg_chapter_verses(rg_book, chapter_num, use_section_visual_separator) diff --git a/backend/doc/domain/assembly_strategies/assembly_strategy_utils.py b/backend/doc/domain/assembly_strategies/assembly_strategy_utils.py index f10ffa6c..55798b18 100755 --- a/backend/doc/domain/assembly_strategies/assembly_strategy_utils.py +++ b/backend/doc/domain/assembly_strategies/assembly_strategy_utils.py @@ -16,10 +16,6 @@ LTR_DIRECTION_HTML: str = "
" RTL_DIRECTION_HTML: str = "
" -TN_VERSE_NOTES_ENCLOSING_DIV_FMT_STR: str = "
{}
" -TQ_HEADING_AND_QUESTIONS_FMT_STR: str = ( - "

{}

\n
{}
" -) CHAPTER_HEADER_FMT_STR: str = '

Chapter {}

' @@ -68,7 +64,7 @@ def chapter_intro( tn_book: Optional[TNBook], chapter_num: int, use_section_visual_separator: bool, - hr: str = "
", + hr: str = settings.HR, ) -> str: """Get the chapter intro.""" content = [] @@ -90,7 +86,7 @@ def has_footnotes(html_content: str) -> bool: def bc_book_intro( bc_book: Optional[BCBook], use_section_visual_separator: bool, - hr: str = "
", + hr: str = settings.HR, ) -> str: content = [] if bc_book and bc_book.book_intro: @@ -103,7 +99,7 @@ def bc_book_intro( def tn_book_intro( tn_book: Optional[TNBook], use_section_visual_separator: bool, - hr: str = "
", + hr: str = settings.HR, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, ) -> str: content = [] @@ -118,7 +114,7 @@ def chapter_commentary( bc_book: Optional[BCBook], chapter_num: int, use_section_visual_separator: bool, - hr: str = "
", + hr: str = settings.HR, ) -> str: """Get the chapter commentary.""" content = [] @@ -181,17 +177,24 @@ def tn_chapter_verses( tn_book: Optional[TNBook], chapter_num: int, use_section_visual_separator: bool, - fmt_str: str = TN_VERSE_NOTES_ENCLOSING_DIV_FMT_STR, - hr: str = "
", + use_two_column_layout_for_tn_notes: bool, + hr: str = settings.HR, ) -> str: """ Return the HTML for verses that are in the chapter with chapter_num. """ + tn_verse_notes_enclosing_div_fmt_str: str = ( + "
{}
" + if use_two_column_layout_for_tn_notes + else "
{}
" + ) content = [] if tn_book and chapter_num in tn_book.chapters: tn_verses = tn_book.chapters[chapter_num].verses - content.append(fmt_str.format("".join(tn_verses.values()))) + content.append( + tn_verse_notes_enclosing_div_fmt_str.format("".join(tn_verses.values())) + ) if use_section_visual_separator: content.append(hr) return "".join(content) @@ -201,16 +204,21 @@ def tq_chapter_verses( tq_book: Optional[TQBook], chapter_num: int, use_section_visual_separator: bool, - fmt_str: str = TQ_HEADING_AND_QUESTIONS_FMT_STR, - hr: str = "
", + use_two_column_layout_for_tq_notes: bool, + hr: str = settings.HR, ) -> str: """Return the HTML for verses in chapter_num.""" + tq_verse_notes_enclosing_div_fmt_str: str = ( + "
{}
" + if use_two_column_layout_for_tq_notes + else "
{}
" + ) content = [] if tq_book and chapter_num in tq_book.chapters: tq_verses = tq_book.chapters[chapter_num].verses content.append( - fmt_str.format( - tq_book.resource_type_name, + tq_verse_notes_enclosing_div_fmt_str.format( + # tq_book.resource_type_name, "".join(tq_verses.values()), ) ) @@ -223,7 +231,7 @@ def rg_chapter_verses( rg_book: Optional[RGBook], chapter_num: int, use_section_visual_separator: bool, - hr: str = "
", + hr: str = settings.HR, ) -> str: """ Return the HTML for verses that are in the chapter with diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py index 49a025c7..93c14c69 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_book_then_lang_by_chapter.py @@ -38,6 +38,8 @@ def assemble_content_by_book_then_lang( assembly_layout_kind: AssemblyLayoutEnum, chunk_size: ChunkSizeEnum, use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> list[DocumentPart]: @@ -90,6 +92,8 @@ def assemble_content_by_book_then_lang( selected_bc_books, selected_rg_books, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) elif not selected_usfm_books and selected_tn_books: @@ -102,6 +106,8 @@ def assemble_content_by_book_then_lang( selected_bc_books, selected_rg_books, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) elif not selected_usfm_books and not selected_tn_books and selected_tq_books: @@ -114,6 +120,7 @@ def assemble_content_by_book_then_lang( selected_bc_books, selected_rg_books, use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) ) elif ( @@ -144,6 +151,8 @@ def assemble_usfm_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, fmt_str: str = settings.BOOK_NAME_FMT_STR, @@ -252,7 +261,10 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tn_book.chapters: tn_verses = tn_chapter_verses( - tn_book, chapter_num, use_section_visual_separator + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, ) if tn_verses: document_parts.append( @@ -260,7 +272,7 @@ def rg_sort_key(resource: RGBook) -> str: content=tn_verses, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tn_notes, add_hr_p=False, use_section_visual_separator=use_section_visual_separator, ) @@ -278,7 +290,10 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tq_book.chapters: tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) if tq_verses: document_parts.append( @@ -286,7 +301,7 @@ def rg_sort_key(resource: RGBook) -> str: content=tq_verses, is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tq_notes, add_hr_p=False, use_section_visual_separator=use_section_visual_separator, ) @@ -330,6 +345,8 @@ def assemble_tn_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, book_chapters: Mapping[str, int] = BOOK_CHAPTERS, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, ) -> list[DocumentPart]: @@ -416,7 +433,10 @@ def rg_sort_key(resource: RGBook) -> str: ]: if chapter_num in tn_book.chapters: tn_verses = tn_chapter_verses( - tn_book, chapter_num, use_section_visual_separator + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, ) if tn_verses: document_parts.append( @@ -424,7 +444,7 @@ def rg_sort_key(resource: RGBook) -> str: content=tn_verses, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tn_notes, add_hr_p=False, use_section_visual_separator=use_section_visual_separator, ) @@ -439,7 +459,10 @@ def rg_sort_key(resource: RGBook) -> str: tq_book for tq_book in tq_books if tq_book.book_code == book_code ]: tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) if tq_verses: document_parts.append( @@ -447,7 +470,7 @@ def rg_sort_key(resource: RGBook) -> str: content=tq_verses, is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tq_notes, add_hr_p=False, use_section_visual_separator=use_section_visual_separator, ) @@ -494,6 +517,7 @@ def assemble_tq_by_chapter( bc_books: Sequence[BCBook], rg_books: Sequence[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tq_notes: bool, book_chapters: Mapping[str, int] = BOOK_CHAPTERS, ) -> list[DocumentPart]: """ @@ -536,7 +560,10 @@ def rg_sort_key(resource: RGBook) -> str: if tq_book.book_code == tq_book.book_code ]: tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) if tq_verses: document_parts.append( @@ -544,7 +571,7 @@ def rg_sort_key(resource: RGBook) -> str: content=tq_verses, is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tq_notes, add_hr_p=False, use_section_visual_separator=use_section_visual_separator, ) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py index 7c3a58fa..15cceafc 100755 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategies_lang_then_book_by_chapter.py @@ -38,6 +38,8 @@ def assemble_content_by_lang_then_book( assembly_layout_kind: AssemblyLayoutEnum, chunk_size: ChunkSizeEnum, use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, book_names: Mapping[str, str] = BOOK_NAMES, book_id_map: dict[str, int] = BOOK_ID_MAP, ) -> list[DocumentPart]: @@ -128,6 +130,8 @@ def assemble_content_by_lang_then_book( bc_book, rg_book, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) elif usfm_book is None and tn_book is not None: @@ -141,6 +145,8 @@ def assemble_content_by_lang_then_book( bc_book, rg_book, use_section_visual_separator, + use_two_column_layout_for_tn_notes, + use_two_column_layout_for_tq_notes, ) ) elif usfm_book is None and tn_book is None and tq_book is not None: @@ -154,6 +160,7 @@ def assemble_content_by_lang_then_book( bc_book, rg_book, use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) ) elif ( @@ -186,6 +193,8 @@ def assemble_usfm_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, fmt_str: str = settings.BOOK_NAME_FMT_STR, ) -> list[DocumentPart]: @@ -234,13 +243,19 @@ def assemble_usfm_by_book( tn_book, chapter_num, use_section_visual_separator ) tn_verses = tn_chapter_verses( - tn_book, chapter_num, use_section_visual_separator + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, ) chapter_commentary_ = chapter_commentary( bc_book, chapter_num, use_section_visual_separator ) tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) rg_verses = rg_chapter_verses( rg_book, chapter_num, use_section_visual_separator @@ -275,7 +290,7 @@ def assemble_usfm_by_book( content=tn_verses, is_rtl=is_rtl, add_hr_p=False, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tn_notes, use_section_visual_separator=use_section_visual_separator, ) ) @@ -291,7 +306,7 @@ def assemble_usfm_by_book( content=tq_verses, is_rtl=is_rtl, add_hr_p=False, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tq_notes, use_section_visual_separator=use_section_visual_separator, ) ) @@ -322,7 +337,7 @@ def assemble_usfm_by_book( content=usfm_book2.chapters[chapter_num].content, is_rtl=usfm_book2 and usfm_book2.lang_direction == LangDirEnum.RTL, - contained_in_two_column_section=True, + contained_in_two_column_section=False, use_section_visual_separator=use_section_visual_separator, ) ) @@ -346,6 +361,8 @@ def assemble_tn_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, show_tn_book_intro: bool = settings.SHOW_TN_BOOK_INTRO, ) -> list[DocumentPart]: """ @@ -389,7 +406,10 @@ def assemble_tn_by_book( ) ) tn_verses = tn_chapter_verses( - tn_book, chapter_num, use_section_visual_separator + tn_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tn_notes, ) if tn_verses: document_parts.append( @@ -397,20 +417,23 @@ def assemble_tn_by_book( content=tn_verses, is_rtl=tn_book and tn_book.lang_direction == LangDirEnum.RTL, add_hr_p=False, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tn_notes, use_section_visual_separator=use_section_visual_separator, ) ) document_parts.append(DocumentPart(content="")) tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) if tq_book and tq_verses: document_parts.append( DocumentPart( content=tq_verses, is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tq_notes, use_section_visual_separator=use_section_visual_separator, ) ) @@ -452,6 +475,7 @@ def assemble_tq_by_book( bc_book: Optional[BCBook], rg_book: Optional[RGBook], use_section_visual_separator: bool, + use_two_column_layout_for_tq_notes: bool, ) -> list[DocumentPart]: """ Construct the HTML for a 'by book' strategy wherein at least @@ -477,14 +501,17 @@ def assemble_tq_by_book( ) ) tq_verses = tq_chapter_verses( - tq_book, chapter_num, use_section_visual_separator + tq_book, + chapter_num, + use_section_visual_separator, + use_two_column_layout_for_tq_notes, ) if tq_verses: document_parts.append( DocumentPart( content=tq_verses, is_rtl=tq_book and tq_book.lang_direction == LangDirEnum.RTL, - contained_in_two_column_section=True, + contained_in_two_column_section=use_two_column_layout_for_tq_notes, use_section_visual_separator=use_section_visual_separator, ) ) diff --git a/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py b/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py index 57529334..b2d0c77b 100644 --- a/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py +++ b/backend/doc/domain/assembly_strategies_docx/assembly_strategy_utils.py @@ -14,10 +14,6 @@ from docx.oxml.ns import qn # type: ignore from docx.oxml.shared import OxmlElement # type: ignore from docx.text.paragraph import Paragraph # type: ignore -from doc.domain.assembly_strategies.assembly_strategy_utils import ( - TN_VERSE_NOTES_ENCLOSING_DIV_FMT_STR, - TQ_HEADING_AND_QUESTIONS_FMT_STR, -) logger = settings.logger(__name__) @@ -126,30 +122,44 @@ def chapter_intro( def tn_chapter_verses( tn_book: Optional[TNBook], chapter_num: int, - fmt_str: str = TN_VERSE_NOTES_ENCLOSING_DIV_FMT_STR, + use_two_column_layout_for_tn_notes: bool, + # fmt_str: str = TN_VERSE_NOTES_ENCLOSING_DIV_FMT_STR, ) -> str: """ Return the HTML for verses that are in the chapter with chapter_num. """ + tn_verse_notes_enclosing_div_fmt_str: str = ( + "
{}
" + if use_two_column_layout_for_tn_notes + else "
{}
" + ) content = [] if tn_book and chapter_num in tn_book.chapters: tn_verses = tn_book.chapters[chapter_num].verses - content.append(fmt_str.format("".join(tn_verses.values()))) + content.append( + tn_verse_notes_enclosing_div_fmt_str.format("".join(tn_verses.values())) + ) return "".join(content) def tq_chapter_verses( tq_book: Optional[TQBook], chapter_num: int, - fmt_str: str = TQ_HEADING_AND_QUESTIONS_FMT_STR, + use_two_column_layout_for_tq_notes: bool, + # fmt_str: str = TQ_HEADING_AND_QUESTIONS_FMT_STR, ) -> str: """Return the HTML for verses in chapter_num.""" + tq_verse_notes_enclosing_div_fmt_str: str = ( + "
{}
" + if use_two_column_layout_for_tq_notes + else "
{}
" + ) content = [] if tq_book and chapter_num in tq_book.chapters: tq_verses = tq_book.chapters[chapter_num].verses content.append( - fmt_str.format( + tq_verse_notes_enclosing_div_fmt_str.format( tq_book.resource_type_name, "".join(tq_verses.values()), ) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index 8fe709f0..8d85e5c2 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -92,6 +92,8 @@ def initialize_document_request_and_key( document_request.limit_words, document_request.use_chapter_labels, document_request.use_section_visual_separator, + document_request.use_two_column_layout_for_tn_notes, + document_request.use_two_column_layout_for_tq_notes, ) return document_request, document_request_key_ @@ -332,6 +334,8 @@ def document_request_key( limit_words: bool, use_chapter_labels: bool, use_section_visual_separator: bool, + use_two_column_layout_for_tn_notes: bool, + use_two_column_layout_for_tq_notes: bool, max_filename_len: int = 240, underscore: str = "_", hyphen: str = "-", @@ -364,9 +368,9 @@ def document_request_key( ] ) if any(contains_tw(resource_request) for resource_request in resource_requests): - document_request_key = f'{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}_{"lwt" if limit_words else "lwf"}_{"sst" if use_section_visual_separator else "ssf"}' + document_request_key = f'{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}_{"lwt" if limit_words else "lwf"}_{"sst" if use_section_visual_separator else "ssf"}_{"2ctn" if use_two_column_layout_for_tn_notes else "1ctn"}_{"2ctq" if use_two_column_layout_for_tq_notes else "1ctq"}' else: - document_request_key = f'{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}_{"sst" if use_section_visual_separator else "ssf"}' + document_request_key = f'{resource_request_keys}_{assembly_strategy_kind.value}_{assembly_layout_kind.value}_{chunk_size.value}_{"clt" if use_chapter_labels else "clf"}_{"sst" if use_section_visual_separator else "ssf"}_{"2ctn" if use_two_column_layout_for_tn_notes else "1ctn"}_{"2ctq" if use_two_column_layout_for_tq_notes else "1ctq"}' if len(document_request_key) >= max_filename_len: # The generated filename could be too long for the OS where this is # running. Therefore, use the current time as a document_request_key @@ -462,6 +466,8 @@ def assemble_content( rg_books, cast(AssemblyLayoutEnum, document_request.assembly_layout_kind), document_request.use_section_visual_separator, + document_request.use_two_column_layout_for_tn_notes, + document_request.use_two_column_layout_for_tq_notes, ) ) elif ( @@ -478,6 +484,8 @@ def assemble_content( rg_books, cast(AssemblyLayoutEnum, document_request.assembly_layout_kind), document_request.use_section_visual_separator, + document_request.use_two_column_layout_for_tn_notes, + document_request.use_two_column_layout_for_tq_notes, ) ) t1 = time.time() @@ -553,6 +561,8 @@ def assemble_docx_content( cast(AssemblyLayoutEnum, document_request.assembly_layout_kind), document_request.chunk_size, document_request.use_section_visual_separator, + document_request.use_two_column_layout_for_tn_notes, + document_request.use_two_column_layout_for_tq_notes, ) elif ( document_request.assembly_strategy_kind @@ -568,6 +578,8 @@ def assemble_docx_content( cast(AssemblyLayoutEnum, document_request.assembly_layout_kind), document_request.chunk_size, document_request.use_section_visual_separator, + document_request.use_two_column_layout_for_tn_notes, + document_request.use_two_column_layout_for_tq_notes, ) t1 = time.time() logger.info("Time for interleaving document: %s", t1 - t0) diff --git a/backend/doc/domain/model.py b/backend/doc/domain/model.py index 9e99114f..6d96c4ac 100644 --- a/backend/doc/domain/model.py +++ b/backend/doc/domain/model.py @@ -214,6 +214,11 @@ class DocumentRequest(BaseModel): # choose to use Princexml rather than Weasyprint by setting use_prince # to True. use_prince: bool = False + # Some languages, e.g., Khmer, don't layout well in 2 column + use_two_column_layout_for_tn_notes: bool = False + # Some languages, e.g., Khmer, don't layout well in 2 column + use_two_column_layout_for_tq_notes: bool = False + # Indicate whether to show visual separator between sections, e.g., hr element use_section_visual_separator: bool = False # Indicate whether TN book intros should be included. Currently, diff --git a/frontend/src/lib/OneColumnLayoutIcon.svelte b/frontend/src/lib/OneColumnLayoutIcon.svelte new file mode 100644 index 00000000..699578f7 --- /dev/null +++ b/frontend/src/lib/OneColumnLayoutIcon.svelte @@ -0,0 +1,18 @@ + + + + + + diff --git a/frontend/src/lib/TwoColumnLayoutIcon.svelte b/frontend/src/lib/TwoColumnLayoutIcon.svelte new file mode 100644 index 00000000..68db2759 --- /dev/null +++ b/frontend/src/lib/TwoColumnLayoutIcon.svelte @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/lib/stores/SettingsStore.ts b/frontend/src/lib/stores/SettingsStore.ts index dc39c365..2d3d0d48 100644 --- a/frontend/src/lib/stores/SettingsStore.ts +++ b/frontend/src/lib/stores/SettingsStore.ts @@ -18,3 +18,5 @@ export let settingsUpdated: Writable = writable(false) export let useChapterLabelsStore: Writable = writable(false) export let useSectionVisualSeparatorStore: Writable = writable(false) export let usePrinceStore: Writable = writable(false) +export let useTwoColumnLayoutForTnNotesStore: Writable = writable(false) +export let useTwoColumnLayoutForTqNotesStore: Writable = writable(false) diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index ecbce21c..27320c2f 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -14,7 +14,9 @@ settingsUpdated, useChapterLabelsStore, useSectionVisualSeparatorStore, - usePrinceStore + usePrinceStore, + useTwoColumnLayoutForTnNotesStore, + useTwoColumnLayoutForTqNotesStore } from '$lib/stores/SettingsStore' import { documentReadyStore, errorStore } from '$lib/stores/NotificationStore' import { @@ -29,6 +31,8 @@ import GenerateDocument from './GenerateDocument.svelte' import LogRocket from 'logrocket' import CheckIcon from '$lib/CheckIcon.svelte' + import OneColumnLayoutIcon from '$lib/OneColumnLayoutIcon.svelte' + import TwoColumnLayoutIcon from '$lib/TwoColumnLayoutIcon.svelte' let showAdvanced = false // Show optional/advanced settings flag @@ -39,23 +43,30 @@ // Set default value of chapter $assemblyStrategyChunkSizeStore = chapter.id - // The 3rd party HTML to PDF conversion library we use, weasyprint, - // doesn't seem to be able to handle line length for the Khmer language - // which results in words overlapping each other when two column - // layout of Khmer content is displayed. Only TN and TQ resource types - // use two column layout and thus if those are selected by the user - // then the UI will exclude PDF as an output format choice for - // Khmer. - let kmRegexp = new RegExp('km, tn, .*|km, tq, .*') - let showPdfAsOption: boolean = true - // $: console.log(`showPdfAsOption: ${showPdfAsOption}`) + // Only show optional settings that are relevant to the resources + // the user has chosen. + let usfmRegex = new RegExp( + 'avd,.*|ayt,.*|blv,.*|cuv,.*|f10,.*|nav,.*|reg,.*|ugnt,.*|uhb,.*|ulb,.*|usfm,.*' + ) + let tnRegex = new RegExp('tn, .*') + let tqRegex = new RegExp('tq, .*') + let showUsfmSettingsAsOption: boolean = false + let showTnTwoColAsOption: boolean = false + let showTqTwoColAsOption: boolean = false $: { if ($resourceTypesStore) { - if ($resourceTypesStore.some((item) => kmRegexp.test(item))) { - showPdfAsOption = false + if ($resourceTypesStore.some((item) => usfmRegex.test(item))) { + showUsfmSettingsAsOption = true + } + if ($resourceTypesStore.some((item) => tnRegex.test(item))) { + showTnTwoColAsOption = true + } + if ($resourceTypesStore.some((item) => tqRegex.test(item))) { + showTqTwoColAsOption = true } } } + $: console.log(`resourceTypesStore: ${$resourceTypesStore}`) $: showEmail = false $: showEmailCaptured = false @@ -143,36 +154,34 @@ ePub
- {#if showPdfAsOption} -
- +
+ +
+ {#if $docTypeStore === 'pdf'} +
+ + Use PrinceXml to produce the PDF (much faster and better quality, but with Prince's 'P' logo at top + right of first page of PDF)
- {#if $docTypeStore === 'pdf'} -
- - Use PrinceXml to produce the PDF (much faster and better quality, but with Prince's 'P' logo at top - right of first page of PDF) -
- {/if} {/if}

Layout

@@ -244,12 +253,14 @@ {#if showAdvanced}

Optional Settings

-
- - Use chapter labels, e.g., 'Chapter 1' instead of '1' -
+ {#if showUsfmSettingsAsOption} +
+ + Use chapter labels, e.g., 'Chapter 1' instead of '1' +
+ {/if}
Show visual separator (horizontal line) between sections
+ {#if showTnTwoColAsOption} +
+ + {#if $useTwoColumnLayoutForTnNotesStore} + Translation notes layout: + +
+ ℹ️ +
+ {:else} + Translation notes layout: + + {/if} +
+ {/if} + {#if showTqTwoColAsOption} +
+ + {#if $useTwoColumnLayoutForTqNotesStore} + Translation questions layout: + +
+ ℹ️ +
+ {:else} + Translation questions layout: + + {/if} +
+ {/if}
{/if}

Notification

diff --git a/frontend/src/routes/settings/GenerateDocument.svelte b/frontend/src/routes/settings/GenerateDocument.svelte index 822a7733..c29ca6ba 100644 --- a/frontend/src/routes/settings/GenerateDocument.svelte +++ b/frontend/src/routes/settings/GenerateDocument.svelte @@ -21,7 +21,9 @@ settingsUpdated, useChapterLabelsStore, useSectionVisualSeparatorStore, - usePrinceStore + usePrinceStore, + useTwoColumnLayoutForTnNotesStore, + useTwoColumnLayoutForTqNotesStore } from '$lib/stores/SettingsStore' import { taskStateStore } from '$lib/stores/TaskStore' import { getCode, getResourceTypeLangCode, getResourceTypeCode } from '$lib/utils' @@ -90,7 +92,9 @@ limit_words: $limitTwStore, use_chapter_labels: $useChapterLabelsStore, use_section_visual_separator: $useSectionVisualSeparatorStore, - use_prince: $usePrinceStore + use_prince: $usePrinceStore, + use_two_column_layout_for_tn_notes: $useTwoColumnLayoutForTnNotesStore, + use_two_column_layout_for_tq_notes: $useTwoColumnLayoutForTqNotesStore } console.log('document request: ', JSON.stringify(documentRequest, null, 2)) $errorStore = null diff --git a/frontend/tests/e2e/test.ts b/frontend/tests/e2e/test.ts index 74167752..dee9123e 100644 --- a/frontend/tests/e2e/test.ts +++ b/frontend/tests/e2e/test.ts @@ -337,4 +337,53 @@ test('test use prince with lots of books', async ({ page }) => { await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() await page.getByText('Use PrinceXml to produce the').click() await page.getByRole('button', { name: 'Generate File' }).click() +test('test visibility of optional settings based on resources chosen', async ({ page }) => { + await page.goto('http://localhost:8001/') + await page.getByText('Français (French)').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Select all').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('Unlocked Literal Bible').click() + await page.getByText('Translation Notes').click() + await page.getByText('Translation Questions').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText('PDF').click() + await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() + await expect(page.getByRole('main')).toContainText( + "Use chapter labels, e.g., 'Chapter 1' instead of '1'" + ) + await page.getByRole('button', { name: 'Edit' }).nth(2).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() + await expect(page.getByRole('main')).toContainText( + "Use 2 column layout for translation notes resource (note: some languages don't layout well with this setting, e.g., Khmer)" + ) + await expect(page.getByRole('main')).toContainText( + "Use 2 column layout for translation questions resource (note: some languages don't layout well with this setting, e.g., Khmer)" + ) + await page.getByRole('button', { name: 'Edit' }).nth(2).click() + await page.getByLabel('Translation Questions tq').uncheck() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() + await expect(page.getByRole('main')).not.toContainText( + "Use 2 column layout for translation questions resource (note: some languages don't layout well with this setting, e.g., Khmer)" + ) + await page.getByRole('button', { name: 'Edit' }).nth(2).click() + await page.getByLabel('Translation Notes tn').uncheck() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() + await expect(page.getByRole('main')).not.toContainText( + "Use 2 column layout for translation notes resource (note: some languages don't layout well with this setting, e.g., Khmer)" + ) + await page.getByRole('button', { name: 'Edit' }).nth(2).click() + await page.getByText('Translation Notes').click() + await page.getByText('Unlocked Literal Bible', { exact: true }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: '▶ Show Optional Settings' }).click() + await expect(page.getByRole('main')).not.toContainText( + "Use chapter labels, e.g., 'Chapter 1' instead of '1'" + ) + await expect(page.getByRole('main')).toContainText( + "Use 2 column layout for translation notes resource (note: some languages don't layout well with this setting, e.g., Khmer)" + ) }) diff --git a/tests/unit/test_document_generator.py b/tests/unit/test_document_generator.py index 2f5b455f..5e55efb7 100644 --- a/tests/unit/test_document_generator.py +++ b/tests/unit/test_document_generator.py @@ -93,5 +93,7 @@ def test_document_request_key_too_long_for_semantic_result() -> None: limit_words, use_chapter_labels, use_section_visual_separator, + use_two_column_layout_for_tn_notes=False, + use_two_column_layout_for_tq_notes=True, ) assert re.search(r"[0-9]+_[0-9]+", key) From e2aa9cf7ea7414ad4e8d797c723a8e3a96a71abd Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Mon, 4 Aug 2025 20:39:39 -0700 Subject: [PATCH 143/208] Rename function --- backend/doc/domain/document_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index 8d85e5c2..ad40dd85 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -680,7 +680,7 @@ def convert_html_to_epub( logger.info("Time for converting HTML to ePub: %s", t1 - t0) -def compose_document( +def compose_docx_document( document_parts: list[DocumentPart], use_section_visual_separator: bool ) -> Document: doc = Document() @@ -739,7 +739,7 @@ def convert_html_to_docx( new_section = doc.add_section(WD_SECTION.CONTINUOUS) new_section.start_type master = Composer(doc) - master.append(compose_document(document_parts, use_section_visual_separator)) + master.append(compose_docx_document(document_parts, use_section_visual_separator)) master.save(docx_filepath) t1 = time.time() logger.info("Time for converting HTML to Docx: %s", t1 - t0) From 4cd041afa1ab529f5d5a9567687efc34d0e04992 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 6 Aug 2025 09:30:49 -0700 Subject: [PATCH 144/208] Update tests Some changes in the data API make it possible to bring some backend tests back online. --- tests/e2e/doc/test_api.py | 57 +++++++++++++-------------------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/tests/e2e/doc/test_api.py b/tests/e2e/doc/test_api.py index 95c73479..085882bf 100644 --- a/tests/e2e/doc/test_api.py +++ b/tests/e2e/doc/test_api.py @@ -54,8 +54,7 @@ def test_en_ulb_col_en_tn_col_language_book_order_with_no_email_1c_docx() -> Non check_result(response, suffix="docx") -@pytest.mark.skip -def test_en_ulb_col_en_tn_col_language_book_order_with_no_email_1c_c() -> None: +def test_fr_ulb_col_fr_tn_col_language_book_order_with_no_email_1c_c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( "/documents", @@ -67,14 +66,15 @@ def test_en_ulb_col_en_tn_col_language_book_order_with_no_email_1c_c() -> None: "generate_pdf": True, "generate_epub": False, "generate_docx": False, + "use_prince": True, "resource_requests": [ { - "lang_code": "en", + "lang_code": "fr", "resource_type": "ulb", "book_code": "col", }, { - "lang_code": "en", + "lang_code": "fr", "resource_type": "tn", "book_code": "col", }, @@ -84,9 +84,7 @@ def test_en_ulb_col_en_tn_col_language_book_order_with_no_email_1c_c() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# tq has been retired for en -@pytest.mark.skip -def test_en_ulb_wa_col_en_tn_wa_col_en_tq_wa_col_language_book_order_1c() -> None: +def test_fr_ulb_wa_col_en_fr_wa_col_fr_tq_wa_col_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( "/documents", @@ -98,19 +96,20 @@ def test_en_ulb_wa_col_en_tn_wa_col_en_tq_wa_col_language_book_order_1c() -> Non "generate_pdf": True, "generate_epub": False, "generate_docx": False, + "use_prince": True, "resource_requests": [ { - "lang_code": "en", + "lang_code": "fr", "resource_type": "ulb", "book_code": "col", }, { - "lang_code": "en", + "lang_code": "fr", "resource_type": "tn", "book_code": "col", }, { - "lang_code": "en", + "lang_code": "fr", "resource_type": "tq", "book_code": "col", }, @@ -120,9 +119,7 @@ def test_en_ulb_wa_col_en_tn_wa_col_en_tq_wa_col_language_book_order_1c() -> Non check_finished_document_with_verses_success(response, suffix="pdf") -# tq has been retired for en -@pytest.mark.skip -def test_en_ulb_col_en_tn_col_en_tq_col_language_book_order_1c_c() -> None: +def test_fr_ulb_col_fr_tn_col_fr_tq_col_language_book_order_1c_c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( "/documents", @@ -134,19 +131,20 @@ def test_en_ulb_col_en_tn_col_en_tq_col_language_book_order_1c_c() -> None: "generate_pdf": True, "generate_epub": False, "generate_docx": False, + "use_prince": True, "resource_requests": [ { - "lang_code": "en", + "lang_code": "fr", "resource_type": "ulb", "book_code": "col", }, { - "lang_code": "en", + "lang_code": "fr", "resource_type": "tn", "book_code": "col", }, { - "lang_code": "en", + "lang_code": "fr", "resource_type": "tq", "book_code": "col", }, @@ -243,8 +241,6 @@ def test_en_ulb_tn_condensed_jud_language_book_order_1c() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# pt-br ulb is no longer provided by the data API -@pytest.mark.skip def test_pt_br_ulb_gen_pt_br_tn_gen_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -257,6 +253,7 @@ def test_pt_br_ulb_gen_pt_br_tn_gen_language_book_order_1c() -> None: "generate_pdf": True, "generate_epub": False, "generate_docx": False, + "use_prince": True, "resource_requests": [ { "lang_code": "pt-br", @@ -274,8 +271,6 @@ def test_pt_br_ulb_gen_pt_br_tn_gen_language_book_order_1c() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# pt-br ulb is no longer provided by the data API -@pytest.mark.skip def test_pt_br_ulb_tn_language_book_order_1c_c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -288,6 +283,7 @@ def test_pt_br_ulb_tn_language_book_order_1c_c() -> None: "generate_pdf": True, "generate_epub": False, "generate_docx": False, + "use_prince": True, "resource_requests": [ { "lang_code": "pt-br", @@ -305,8 +301,6 @@ def test_pt_br_ulb_tn_language_book_order_1c_c() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# pt-br ulb is no longer provided by the data API -@pytest.mark.skip def test_pt_br_ulb_tn_en_ulb_tn_luk_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -319,6 +313,7 @@ def test_pt_br_ulb_tn_en_ulb_tn_luk_language_book_order_1c() -> None: "generate_pdf": True, "generate_epub": False, "generate_docx": False, + "use_prince": True, "resource_requests": [ { "lang_code": "pt-br", @@ -346,8 +341,6 @@ def test_pt_br_ulb_tn_en_ulb_tn_luk_language_book_order_1c() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# pt-br ulb is no longer provided by the data API -@pytest.mark.skip def test_pt_br_ulb_tn_en_ulb_tn_luk_language_book_order_1c_c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -360,6 +353,7 @@ def test_pt_br_ulb_tn_en_ulb_tn_luk_language_book_order_1c_c() -> None: "generate_pdf": True, "generate_epub": False, "generate_docx": False, + "use_prince": True, "resource_requests": [ { "lang_code": "pt-br", @@ -387,8 +381,6 @@ def test_pt_br_ulb_tn_en_ulb_tn_luk_language_book_order_1c_c() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# pt-br ulb is no longer provided by the data API -@pytest.mark.skip def test_pt_br_ulb_tn_luk_en_ulb_tn_luk_sw_ulb_tn_col_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -401,6 +393,7 @@ def test_pt_br_ulb_tn_luk_en_ulb_tn_luk_sw_ulb_tn_col_language_book_order_1c() - "generate_pdf": True, "generate_epub": False, "generate_docx": False, + "use_prince": True, "resource_requests": [ { "lang_code": "pt-br", @@ -438,9 +431,6 @@ def test_pt_br_ulb_tn_luk_en_ulb_tn_luk_sw_ulb_tn_col_language_book_order_1c() - check_finished_document_with_verses_success(response, suffix="pdf") -# More than two languages are no longer allowed as we enforce that in the DocumentRequest class via pydnatic validation. -# pt-br ulb is no longer provided by the data API -@pytest.mark.skip def test_pt_br_ulb_tn_luk_en_ulb_tn_luk_sw_ulb_tn_col_language_book_order_1c_c() -> ( None ): @@ -1417,8 +1407,6 @@ def test_zh_cuv_jol_zh_tn_jol_zh_tq_jol_zh_tw_jol_language_book_order_1c_c() -> check_finished_document_with_verses_success(response, suffix="pdf") -# pt-br ulb is no longer provided by the data API -@pytest.mark.skip def test_pt_br_ulb_luk_pt_br_tn_luk_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -1426,7 +1414,6 @@ def test_pt_br_ulb_luk_pt_br_tn_luk_language_book_order_1c() -> None: json={ "email_address": settings.TO_EMAIL_ADDRESS, "assembly_strategy_kind": model.AssemblyStrategyEnum.LANGUAGE_BOOK_ORDER, - # "assembly_layout_kind": model.AssemblyLayoutEnum.ONE_COLUMN, "assembly_layout_kind": None, "layout_for_print": False, "generate_pdf": True, @@ -1449,8 +1436,6 @@ def test_pt_br_ulb_luk_pt_br_tn_luk_language_book_order_1c() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# pt-br ulb is no longer provided by the data API -@pytest.mark.skip def test_pt_br_ulb_luk_pt_br_tn_luk_language_book_order_1c_c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -1598,7 +1583,6 @@ def test_bjz_reg_eph_lbo_1c_chapter() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# @pytest.mark.focus def test_bys_reg_col_lbo_1c_chapter() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -1623,7 +1607,6 @@ def test_bys_reg_col_lbo_1c_chapter() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# @pytest.mark.focus def test_en_ulb_col_bys_reg_col_blo_1c_chapter() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -1653,7 +1636,6 @@ def test_en_ulb_col_bys_reg_col_blo_1c_chapter() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# @pytest.mark.focus def test_en_ulb_col_bjz_reg_col_blo_1c_chapter() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( @@ -1683,7 +1665,6 @@ def test_en_ulb_col_bjz_reg_col_blo_1c_chapter() -> None: check_finished_document_with_verses_success(response, suffix="pdf") -# @pytest.mark.focus def test_en_ulb_eph_bys_reg_eph_blo_1c_chapter_docx() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( From d7b90f7af3351fe28c10466a4ade189a206f2e0c Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Wed, 6 Aug 2025 09:32:53 -0700 Subject: [PATCH 145/208] Dynamic header in PDF document Put bible book name at top center and chapter and chapter number at top right. --- backend/doc/config.py | 2 +- backend/templates/html/header_enclosing.html | 32 ++++++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/backend/doc/config.py b/backend/doc/config.py index 67bcb7d5..fb1db2de 100755 --- a/backend/doc/config.py +++ b/backend/doc/config.py @@ -41,7 +41,7 @@ class Settings(BaseSettings): USE_LOCALIZED_BOOK_NAME: bool CHECK_ALL_BOOKS_FOR_LANGUAGE: bool - BOOK_NAME_FMT_STR: str = "

{}

" + BOOK_NAME_FMT_STR: str = "

{}

" END_OF_CHAPTER_HTML: str = '
' HR: str = "
" diff --git a/backend/templates/html/header_enclosing.html b/backend/templates/html/header_enclosing.html index 105377a6..1ee0449b 100644 --- a/backend/templates/html/header_enclosing.html +++ b/backend/templates/html/header_enclosing.html @@ -4,19 +4,24 @@ Bible diff --git a/frontend/src/routes/passages/passages/AddOTComponent.svelte b/frontend/src/routes/passages/passages/AddOTComponent.svelte new file mode 100644 index 00000000..c4ddc619 --- /dev/null +++ b/frontend/src/routes/passages/passages/AddOTComponent.svelte @@ -0,0 +1,618 @@ + + +
+ + +
+
+ + +
+ {#if isLoadingOTSurveyRG1} +
+ {:else if otSurveyRG1SuccessMessage} +
+ {@html checkIcon} +
+ {/if} +
+
+
+ + +
+ {#if isLoadingOTSurveyRG2} +
+ {:else if otSurveyRG2SuccessMessage} +
+ {@html checkIcon} +
+ {/if} +
+
+
+ + +
+ {#if isLoadingOTSurveyRG3} +
+ {:else if otSurveyRG3SuccessMessage} +
+ {@html checkIcon} +
+ {/if} +
+
+
+ + +
+ {#if isLoadingOTSurveyRG4} +
+ {:else if otSurveyRG4SuccessMessage} +
+ {@html checkIcon} +
+ {/if} +
+
+ + diff --git a/frontend/src/routes/passages/passages/AddPassageComponent.svelte b/frontend/src/routes/passages/passages/AddPassageComponent.svelte new file mode 100644 index 00000000..ced8dbfc --- /dev/null +++ b/frontend/src/routes/passages/passages/AddPassageComponent.svelte @@ -0,0 +1,158 @@ + + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ {#if passageSuccessMessage} +
+ {@html checkIcon} +
+ {/if} +
+
+ + diff --git a/frontend/src/routes/passages/passages/AddSTETComponent.svelte b/frontend/src/routes/passages/passages/AddSTETComponent.svelte new file mode 100644 index 00000000..eab4d7d8 --- /dev/null +++ b/frontend/src/routes/passages/passages/AddSTETComponent.svelte @@ -0,0 +1,168 @@ + + +
+ + +
+ {#if isLoadingStetPassages} +
+ {:else if stetSuccessMessage} +
+ {@html checkIcon} +
+ {/if} +
+
+ + diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index f41bbb6d..97050969 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -1,34 +1,21 @@
-
-
- - -
-
- - -
-
- - -
- -
- {#if passageSuccessMessage} -
- {@html checkIcon} -
- {/if} -
-
-
- - -
- {#if isLoadingNTSurvey} -
- {:else if ntSurveySuccessMessage} -
- {@html checkIcon} -
- {/if} -
-
-
- - -
- {#if isLoadingStetPassages} -
- {:else if stetSuccessMessage} -
- {@html checkIcon} -
- {/if} -
-
+ + + + {#if !is_production} + + {/if}
- - diff --git a/frontend/tests/e2e/passages_test.ts b/frontend/tests/e2e/passages_test.ts index 835cef3f..40daf88f 100644 --- a/frontend/tests/e2e/passages_test.ts +++ b/frontend/tests/e2e/passages_test.ts @@ -1,26 +1,26 @@ import { test, expect } from '@playwright/test' -test('test add passages', async ({ page }) => { - await page.goto('http://localhost:8001/passages') - await page.getByText('Español Latin America (Latin').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByLabel('Bible Book').selectOption('jos') - await page.getByLabel('Chapter').selectOption('7') - await page.getByPlaceholder('e.g., 1,2,5-').click() - await page.getByPlaceholder('e.g., 1,2,5-').fill('1-10') - await page.getByRole('button', { name: 'Add Passage' }).click() - await page.getByLabel('Bible Book').selectOption('est') - await page.getByLabel('Chapter').selectOption('7') - await page.getByPlaceholder('e.g., 1,2,5-').click() - await page.getByPlaceholder('e.g., 1,2,5-').fill('3,12') - await page.getByRole('button', { name: 'Add Passage' }).click() - await page.getByLabel('Bible Book').selectOption('1th') - await page.getByLabel('Chapter').selectOption('4') - await page.getByPlaceholder('e.g., 1,2,5-').click() - await page.getByPlaceholder('e.g., 1,2,5-').fill('1,5-8,11') - await page.getByRole('button', { name: 'Add Passage' }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Generate File' }).click() +test('add passages', async ({ page }) => { + await page.goto('http://localhost:8001/passages') + await page.getByText('Español Latin America (Latin').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByLabel('Bible Book').selectOption('jos') + await page.getByLabel('Chapter').selectOption('7') + await page.getByPlaceholder('e.g., 1,2,5-').click() + await page.getByPlaceholder('e.g., 1,2,5-').fill('1-10') + await page.getByRole('button', { name: 'Add Passage' }).click() + await page.getByLabel('Bible Book').selectOption('est') + await page.getByLabel('Chapter').selectOption('7') + await page.getByPlaceholder('e.g., 1,2,5-').click() + await page.getByPlaceholder('e.g., 1,2,5-').fill('3,12') + await page.getByRole('button', { name: 'Add Passage' }).click() + await page.getByLabel('Bible Book').selectOption('1th') + await page.getByLabel('Chapter').selectOption('4') + await page.getByPlaceholder('e.g., 1,2,5-').click() + await page.getByPlaceholder('e.g., 1,2,5-').fill('1,5-8,11') + await page.getByRole('button', { name: 'Add Passage' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('button', { name: 'Generate File' }).click() }) test('test that you can select gateway tab after first selecting heart language and hitting next', async ({ @@ -38,14 +38,77 @@ test('test that you can select gateway tab after first selecting heart language await expect(page.locator('body')).toContainText('Matius 2:1-12') await page.getByRole('button', { name: 'Next' }).click() await expect(page.locator('body')).toContainText('Matius 2:1-12') +test('stet passages not available in production', async ({ page }) => { + await page.addInitScript(() => { + Object.defineProperty(window, 'location', { + value: { + ...window.location, + hostname: 'bibleineverylanguage.org' + }, + configurable: true + }) + }) + await page.goto('http://localhost:8001/passages') + await page.getByText('Cebuano').click() + await page.getByRole('button', { name: 'Next' }).click() + await expect(page.locator('#stet-passages')).not.toBeVisible() }) -test('test add stet verse list to passages', async ({ page }) => { - await page.goto('http://localhost:8001/passages') - await page.getByText('Cebuano').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Add STET Passages').click() - await page.getByText('Mateo 1:1', { exact: true }).click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('button', { name: 'Generate File' }).click() +test('stet passages available when not in production', async ({ page }) => { + await page.addInitScript(() => { + Object.defineProperty(window, 'location', { + value: { + ...window.location, + hostname: 'walink.org' + }, + configurable: true + }) + }) + await page.goto('http://localhost:8001/passages') + await page.getByText('Cebuano').click() + await page.getByRole('button', { name: 'Next' }).click() + // Wait for the element that proves loading is done + await page.waitForSelector('#stet-passages', { state: 'visible' }) + await expect(page.locator('#stet-passages')).toBeVisible() +}) + +test('passages checkboxes', async ({ page }) => { + await page.goto('http://localhost:8001/passages') + await page.getByText('English').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByLabel('Add OT Survey RG1 Passages').check() + await expect(page.locator('body')).toContainText('Genesis 1:1-2', { timeout: 32_000 }) + await page.getByLabel('Add OT Survey RG1 Passages').uncheck() + await expect(page.locator('body')).not.toContainText('Genesis 1:1-2') + await page.getByLabel('Add OT Survey RG2 Passages').check() + await expect(page.locator('body')).toContainText('Joshua 1:1-9', { timeout: 32_000 }) + await page.getByLabel('Add OT Survey RG2 Passages').uncheck() + await expect(page.locator('body')).not.toContainText('Joshua 1:1-9', { timeout: 32_000 }) + await page.getByLabel('Add OT Survey RG3 Passages').check() + await expect(page.locator('body')).toContainText('Job 1:6-22') + await page.getByLabel('Add OT Survey RG3 Passages').uncheck() + await expect(page.locator('body')).not.toContainText('Job 1:6-22') + await page.getByLabel('Add OT Survey RG4 Passages').check() + await expect(page.locator('body')).toContainText('Isaiah 1:1-9', { timeout: 32_000 }) + await page.getByLabel('Add OT Survey RG4 Passages').uncheck() + await expect(page.locator('body')).not.toContainText('Isaiah 1:1-9', { timeout: 32_000 }) + await page.getByLabel("Add NT Survey Reviewers'").check() + await expect(page.locator('body')).toContainText('Matthew 2:1-12', { timeout: 32_000 }) + await page.getByLabel("Add NT Survey Reviewers'").uncheck() + await expect(page.locator('body')).not.toContainText('Matthew 2:1-12', { timeout: 32_000 }) + await page.getByLabel('Add STET Passages').check() + await expect(page.locator('body')).toContainText('Matthew 1:1', { timeout: 32_000 }) + await page.getByLabel('Add STET Passages').uncheck() + await expect(page.locator('body')).not.toContainText('Matthew 1:1', { timeout: 32_000 }) + await page.getByLabel('Add all OT RG Passages').check() + await expect(page.locator('body')).toContainText('Genesis 1:1-2', { timeout: 32_000 }) + await page.getByLabel('Add all OT RG Passages').uncheck() + await expect(page.locator('body')).not.toContainText('Genesis 1:1-2', { timeout: 32_000 }) + await page.getByLabel('Bible Book').selectOption('lev') + await page.getByLabel('Chapter').selectOption('6') + await page.getByPlaceholder('e.g., 1,2,5-').click() + await page.getByPlaceholder('e.g., 1,2,5-').fill('1,2,5-7') + await page.getByPlaceholder('e.g., 1,2,5-').press('Tab') + await page.getByRole('button', { name: 'Add Passage' }).click() + await expect(page.locator('body')).toContainText('Leviticus 6:1,2,5-7') }) From 0e824a529c778cda434db6a672cee4e94602646d Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 18 Sep 2025 15:04:32 -0700 Subject: [PATCH 199/208] Better test naming style --- frontend/tests/e2e/passages_test.ts | 4 +-- frontend/tests/e2e/stet_test.ts | 14 ++++----- frontend/tests/e2e/test.ts | 48 +++++++++++++---------------- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/frontend/tests/e2e/passages_test.ts b/frontend/tests/e2e/passages_test.ts index 40daf88f..8170bdc7 100644 --- a/frontend/tests/e2e/passages_test.ts +++ b/frontend/tests/e2e/passages_test.ts @@ -23,8 +23,8 @@ test('add passages', async ({ page }) => { await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test that you can select gateway tab after first selecting heart language and hitting next', async ({ - page +test('select gateway tab after first selecting heart language and hitting next', async ({ + page }) => { await page.goto('http://localhost:8001/passages') await page.getByRole('button', { name: 'Heart' }).click() diff --git a/frontend/tests/e2e/stet_test.ts b/frontend/tests/e2e/stet_test.ts index 5559a22f..12d7edd4 100644 --- a/frontend/tests/e2e/stet_test.ts +++ b/frontend/tests/e2e/stet_test.ts @@ -9,7 +9,7 @@ test.describe('Desktop Tests', () => { } }) - test('test stet', async ({ page }) => { + test('stet', async ({ page }) => { await page.goto('http://localhost:8001/stet') await page.getByLabel('English en').check() await page.getByRole('button', { name: 'Next' }).click() @@ -18,7 +18,7 @@ test.describe('Desktop Tests', () => { await page.getByRole('button', { name: 'Generate File' }).click() }) - test('test french stet', async ({ page }) => { + test('french stet', async ({ page }) => { await page.goto('http://localhost:8001/stet') await page.getByText('Français (French)').click() await page.getByRole('button', { name: 'Next' }).click() @@ -27,7 +27,7 @@ test.describe('Desktop Tests', () => { await page.getByRole('button', { name: 'Generate File' }).click() }) - test('test search by language code', async ({ page }) => { + test('search by language code', async ({ page }) => { await page.goto('http://localhost:8001/stet') await page.getByText('Português Brasileiro (').click() await page.getByRole('button', { name: 'Next' }).click() @@ -39,7 +39,7 @@ test.describe('Desktop Tests', () => { await expect(page.locator('body')).toContainText('Sègbé(sxw-x-segbe)') }) - test('test next back and edit', async ({ page }) => { + test('next then back and edit', async ({ page }) => { await page.goto('http://localhost:8001/stet') await page.getByText('English').click() await page.getByRole('button', { name: 'Next' }).click() @@ -64,7 +64,7 @@ test.describe('Desktop Tests', () => { await page.getByRole('button', { name: 'Generate File' }).click() }) - test('test tok pisin input language', async ({ page }) => { + test('tok pisin input language', async ({ page }) => { await page.goto('http://localhost:8001/stet') await page.getByText('Tok Pisin').click() await page.getByRole('button', { name: 'Next' }).click() @@ -83,7 +83,7 @@ test.describe('Mobile Tests', () => { } }) - test('test mobile', async ({ page }) => { + test('mobile', async ({ page }) => { await page.goto('http://localhost:8001/stet') await page.getByLabel('English en').check() await page.getByRole('button').nth(1).click() @@ -95,7 +95,7 @@ test.describe('Mobile Tests', () => { await page.getByRole('button', { name: 'Generate File' }).click() }) - test('test mobile 2', async ({ page }) => { + test('mobile 2', async ({ page }) => { await page.goto('http://localhost:8001/stet') await page.getByText('English').click() await page.getByRole('button').nth(1).click() diff --git a/frontend/tests/e2e/test.ts b/frontend/tests/e2e/test.ts index a5504b38..25878bc2 100644 --- a/frontend/tests/e2e/test.ts +++ b/frontend/tests/e2e/test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' -test('test ui part 1', async ({ page }) => { +test('ui part 1', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByText('Tiếng Việt (Vietnamese)').click() await page.getByRole('button', { name: 'Next' }).click() @@ -25,7 +25,7 @@ test('test ui part 1', async ({ page }) => { await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test ui part 2', async ({ page }) => { +test('ui part 2', async ({ page }) => { await page.goto('http://localhost:8001') await page.getByText('English').click() await page.getByText('Español Latin America (Latin').click() @@ -54,9 +54,7 @@ test('test ui part 2', async ({ page }) => { await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test books retained in basket on back button to languages and then forward', async ({ - page -}) => { +test('books retained in basket on back button to languages and then forward', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByPlaceholder('Search Gateway Languages').click() await page.getByPlaceholder('Search Gateway Languages').fill('Amh') @@ -82,7 +80,7 @@ test('test books retained in basket on back button to languages and then forward await page.getByPlaceholder('Search NT books').fill('2 ኛ ዮሐንስ') }) -test('test transfer from biel', async ({ page }) => { +test('transfer from biel', async ({ page }) => { await page.goto( 'http://localhost:8001/transfer/repo_url=https%3A%2F%2Fcontent.bibletranslationtools.org%2Fchunga_moses%2Fleb-x-bisa_col_text_reg&book_name=Colossians' ) @@ -90,7 +88,7 @@ test('test transfer from biel', async ({ page }) => { await expect(page.getByText('Colossians')).toBeVisible({ timeout: 1200000 }) }) -test('test transfer from biel 2', async ({ page }) => { +test('transfer from biel 2', async ({ page }) => { await page.goto( 'http://localhost:8001/transfer/repo_url=https:%2F%2Fcontent.bibletranslationtools.org%2FWycliffeAssociates%2Fen_ulb' ) @@ -100,7 +98,7 @@ test('test transfer from biel 2', async ({ page }) => { await expect(page.getByText('(60) items hidden')).toBeVisible() }) -test('test es-419 resource types', async ({ page }) => { +test('es-419 resource types', async ({ page }) => { await page.goto('http://localhost:8001') await page.getByText(/.*Español.*/).click() await page.getByRole('button', { name: 'Next' }).click() @@ -113,9 +111,7 @@ test('test es-419 resource types', async ({ page }) => { await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test that reviewers guide is only shown when book is chosen that it includes', async ({ - page -}) => { +test('reviewers guide is only shown when book is chosen that it includes', async ({ page }) => { await page.goto('http://localhost:8001/languages') await page.getByText('English').click() await page.getByRole('button', { name: 'Next' }).click() @@ -126,7 +122,7 @@ test('test that reviewers guide is only shown when book is chosen that it includ ).not.toBeVisible({ timeout: 580000 }) }) -test('test that reviewers guide is only shown when book is chosen that it includes - part 2', async ({ +test('reviewers guide is only shown when book is chosen that it includes - part 2', async ({ page }) => { await page.goto('http://localhost:8001/languages') @@ -139,7 +135,7 @@ test('test that reviewers guide is only shown when book is chosen that it includ }) }) -test('test that you can select gateway tab after first selecting heart language and hitting next', async ({ +test('can select gateway tab after first selecting heart language and hitting next', async ({ page }) => { await page.goto('http://localhost:8001/') @@ -160,7 +156,7 @@ test('test that you can select gateway tab after first selecting heart language await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test optional settings', async ({ page }) => { +test('optional settings', async ({ page }) => { await page.goto('http://localhost:8001/languages') await page.getByText('Bichelamar (Bislama)').click() await page.getByRole('button', { name: 'Next' }).click() @@ -176,7 +172,7 @@ test('test optional settings', async ({ page }) => { ) }) -test.skip('test aba philemon', async ({ page }) => { +test.skip('aba philemon', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByRole('button', { name: 'Heart' }).click() await page.getByText('Abé aba').click() @@ -191,7 +187,7 @@ test.skip('test aba philemon', async ({ page }) => { await expect(page.locator('body')).toContainText('Regular (aba)') }) -test('test that book name correction happened for pt-br, 1co', async ({ page }) => { +test('book name correction happened for pt-br, 1co', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByText('Português Brasileiro (').click() await page.getByRole('button', { name: 'Next' }).click() @@ -199,7 +195,7 @@ test('test that book name correction happened for pt-br, 1co', async ({ page }) await expect(page.locator('body')).toContainText('1 Coríntios') }) -test('test limit tw words switch', async ({ page }) => { +test('limit tw words switch', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByText('Cebuano').click() await page.getByRole('button', { name: 'Next' }).click() @@ -225,7 +221,7 @@ test('test limit tw words switch', async ({ page }) => { await expect(page.getByRole('main')).toContainText('Limit TW words') }) -test('test use section visual separator setting', async ({ page }) => { +test('use section visual separator setting', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByText('Español Latin America (Latin').click() await page.getByRole('button', { name: 'Next' }).click() @@ -238,7 +234,7 @@ test('test use section visual separator setting', async ({ page }) => { await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test ordering of books in document title(s) and body', async ({ page }) => { +test('ordering of books in document title(s) and body', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByPlaceholder('Search Gateway Languages').click() await page.getByPlaceholder('Search Gateway Languages').fill('tpi') @@ -311,7 +307,7 @@ test('test ordering of books in document title(s) and body', async ({ page }) => expect(index4).toBeLessThan(index5) }) -test('test languages are sorted in clicked order', async ({ page }) => { +test('languages are sorted in clicked order', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByText('Français (French)').click() await page.getByText('Cebuano').click() @@ -341,7 +337,7 @@ test('test languages are sorted in clicked order', async ({ page }) => { ) }) -test('test use prince with lots of books', async ({ page }) => { +test('use prince with lots of books', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByPlaceholder('Search Gateway Languages').click() await page.getByPlaceholder('Search Gateway Languages').fill('tpi') @@ -365,7 +361,7 @@ test('test use prince with lots of books', async ({ page }) => { await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test acq', async ({ page }) => { +test('acq', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByRole('button', { name: 'Heart' }).click() await page.getByPlaceholder('Search Heart Languages').click() @@ -385,7 +381,7 @@ test('test acq', async ({ page }) => { await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test visibility of optional settings based on resources chosen', async ({ page }) => { +test('visibility of optional settings based on resources chosen', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByText('Français (French)').click() await page.getByRole('button', { name: 'Next' }).click() @@ -426,7 +422,7 @@ test('test visibility of optional settings based on resources chosen', async ({ ) }) -test('test burmese', async ({ page }) => { +test('burmese', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByPlaceholder('Search Gateway Languages').click() await page.getByPlaceholder('Search Gateway Languages').fill('my') @@ -449,7 +445,7 @@ test('test burmese', async ({ page }) => { await page.getByRole('button', { name: 'Generate File' }).click() }) -test('test merge of data API data and DOC only data', async ({ page }) => { +test('merge of data API data and DOC only data', async ({ page }) => { await page.goto('http://localhost:8001/') await expect(page.getByRole('main')).toContainText('Bahasa Indonesia (Indonesian)') await page.getByLabel('Bahasa Indonesia (Indonesian').check() @@ -460,7 +456,7 @@ test('test merge of data API data and DOC only data', async ({ page }) => { await expect(page.locator('body')).toContainText('Translation Notes') }) -test('test space between end of chunk and beginning of another', async ({ page }) => { +test('space between end of chunk and beginning of another', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByRole('button', { name: 'Heart' }).click() await page.getByPlaceholder('Search Heart Languages').click() From 820ebf9a0ca5a83661c3bfcdef541d48c944eda5 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 18 Sep 2025 15:06:02 -0700 Subject: [PATCH 200/208] Add a couple timeouts to aide e2e tests --- frontend/tests/e2e/passages_test.ts | 4 ++-- frontend/tests/e2e/test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/tests/e2e/passages_test.ts b/frontend/tests/e2e/passages_test.ts index 8170bdc7..916aeda3 100644 --- a/frontend/tests/e2e/passages_test.ts +++ b/frontend/tests/e2e/passages_test.ts @@ -35,9 +35,9 @@ test('select gateway tab after first selecting heart language and hitting next', await page.getByText('Bahasa Indonesia (Indonesian)').click() await page.getByRole('button', { name: 'Next' }).click() await page.getByText("Add NT Survey Reviewers'").click() - await expect(page.locator('body')).toContainText('Matius 2:1-12') await page.getByRole('button', { name: 'Next' }).click() - await expect(page.locator('body')).toContainText('Matius 2:1-12') + await expect(page.getByText('Matius 2:1-12')).toBeVisible({ timeout: 32000 }) + await expect(page.getByText('Matius 2:1-12')).toBeVisible() test('stet passages not available in production', async ({ page }) => { await page.addInitScript(() => { Object.defineProperty(window, 'location', { diff --git a/frontend/tests/e2e/test.ts b/frontend/tests/e2e/test.ts index 25878bc2..d812831a 100644 --- a/frontend/tests/e2e/test.ts +++ b/frontend/tests/e2e/test.ts @@ -14,7 +14,7 @@ test('ui part 1', async ({ page }) => { await page.getByText('অসমীয়া (Assamese)').click() await page.getByRole('button', { name: 'Next' }).click() await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Unlocked Literal Bible').nth(1).click() + await page.getByText('Unlocked Literal Bible').nth(1).click({ timeout: 32_000 }) await page.getByText('Translation Notes tn').first().click() await page.getByText('Translation Notes').nth(1).click() await page.getByText('Translation Questions').first().click() From 58aaef0c5d5c366a62eea0c43a5d6e75c88911fa Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 18 Sep 2025 15:06:46 -0700 Subject: [PATCH 201/208] Autoformatting via prettier --- frontend/tests/e2e/passages_test.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/tests/e2e/passages_test.ts b/frontend/tests/e2e/passages_test.ts index 916aeda3..a3a53621 100644 --- a/frontend/tests/e2e/passages_test.ts +++ b/frontend/tests/e2e/passages_test.ts @@ -26,18 +26,20 @@ test('add passages', async ({ page }) => { test('select gateway tab after first selecting heart language and hitting next', async ({ page }) => { - await page.goto('http://localhost:8001/passages') - await page.getByRole('button', { name: 'Heart' }).click() - await page.getByText('Abure abu').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByRole('link', { name: 'Language' }).click() - await page.getByRole('button', { name: 'Gateway' }).click() - await page.getByText('Bahasa Indonesia (Indonesian)').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText("Add NT Survey Reviewers'").click() - await page.getByRole('button', { name: 'Next' }).click() + await page.goto('http://localhost:8001/passages') + await page.getByRole('button', { name: 'Heart' }).click() + await page.getByText('Abure abu').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByRole('link', { name: 'Language' }).click() + await page.getByRole('button', { name: 'Gateway' }).click() + await page.getByText('Bahasa Indonesia (Indonesian)').click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByText("Add NT Survey Reviewers'").click() await expect(page.getByText('Matius 2:1-12')).toBeVisible({ timeout: 32000 }) + await page.getByRole('button', { name: 'Next' }).click() await expect(page.getByText('Matius 2:1-12')).toBeVisible() +}) + test('stet passages not available in production', async ({ page }) => { await page.addInitScript(() => { Object.defineProperty(window, 'location', { From 9e58d021ab41f02b8f110e382b43bb41b4bb4b6d Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 19 Sep 2025 19:07:04 -0700 Subject: [PATCH 202/208] Add backend tests for OT RG endpoints --- tests/unit/test_resource_lookup_api.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit/test_resource_lookup_api.py b/tests/unit/test_resource_lookup_api.py index 2adf5e4a..743badd2 100644 --- a/tests/unit/test_resource_lookup_api.py +++ b/tests/unit/test_resource_lookup_api.py @@ -69,3 +69,20 @@ def test_lookup_failures() -> None: def test_nt_survey_rg_passages() -> None: bible_references = resource_lookup.nt_survey_rg_passages() assert bible_references + + +def test_en_ot_survey_rg1_passages() -> None: + bible_references = resource_lookup.ot_survey_rg1_passages() + assert bible_references + +def test_en_ot_survey_rg2_passages() -> None: + bible_references = resource_lookup.ot_survey_rg2_passages() + assert bible_references + +def test_en_ot_survey_rg3_passages() -> None: + bible_references = resource_lookup.ot_survey_rg3_passages() + assert bible_references + +def test_en_ot_survey_rg4_passages() -> None: + bible_references = resource_lookup.ot_survey_rg4_passages() + assert bible_references From c64037c00333757d61988c462b091bf4e2b321e4 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 19 Sep 2025 19:08:39 -0700 Subject: [PATCH 203/208] Rename variable --- .../routes/passages/passages/BibleReferenceSelector.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index 97050969..fac174a8 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -7,7 +7,7 @@ import AddNTComponent from './AddNTComponent.svelte' import AddSTETComponent from './AddSTETComponent.svelte' - let is_production = window.location.hostname.includes(PUBLIC_PRODUCTION_DOMAIN) ? true : false + let isProduction = window.location.hostname.includes(PUBLIC_PRODUCTION_DOMAIN) ? true : false let loading = false let checkIcon = '' @@ -43,7 +43,7 @@ - {#if !is_production} + {#if !isProduction} {/if}
From fa00d10b5ef64268ff1ca494a979ee20a657834b Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 19 Sep 2025 19:08:50 -0700 Subject: [PATCH 204/208] Remove a log output statement --- .../src/routes/passages/passages/BibleReferenceSelector.svelte | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte index fac174a8..f5a56d47 100644 --- a/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte +++ b/frontend/src/routes/passages/passages/BibleReferenceSelector.svelte @@ -15,8 +15,6 @@ export let chapters: Record = {} export let bookCodesAndNames: [string, string][] - $: console.log(`is_production: ${is_production}`) - onMount(() => { getChaptersInBooks() .then((chaptersInBooks_) => { From 00dcee9aad359f50c46a9424cacf6070268e104a Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 19 Sep 2025 19:09:41 -0700 Subject: [PATCH 205/208] Limit STET source languages in production to subset Only show STET source languages that provide 4th column (boldings) in their input document --- backend/stet/entrypoints/routes.py | 34 ++++++++---- .../routes/stet/source_languages/+page.svelte | 11 +++- frontend/tests/e2e/stet_test.ts | 52 ++++++++++++++++++- 3 files changed, 84 insertions(+), 13 deletions(-) diff --git a/backend/stet/entrypoints/routes.py b/backend/stet/entrypoints/routes.py index 9b686d33..32f02f14 100644 --- a/backend/stet/entrypoints/routes.py +++ b/backend/stet/entrypoints/routes.py @@ -1,16 +1,15 @@ from os import scandir from typing import Sequence -from fastapi import APIRouter import celery.states from celery.result import AsyncResult from doc.config import settings -from stet.domain import document_generator, model from doc.domain import resource_lookup - -from fastapi import HTTPException, status - +from docx import Document # type: ignore +from fastapi import APIRouter, Request, HTTPException, status from fastapi.responses import JSONResponse +from stet.domain import document_generator, model + router = APIRouter() @@ -19,11 +18,14 @@ @router.get("/stet/source_languages") async def source_lang_codes_and_names( + request: Request, stet_dir: str = settings.STET_DIR, ) -> Sequence[tuple[str, str, bool]]: """ Return list of all available language code, name tuples for which Translation Services has provided a source document. """ + is_production = request.headers.get("x-is-production") == "true" + logger.debug("is_production: %s", is_production) # Scan what source docs are available and make sure to filter # source language candidates to only those languages. ietf_codes = [ @@ -34,11 +36,23 @@ async def source_lang_codes_and_names( and entry.name.endswith(".docx") ] # logger.debug("source ietf_codes: %s", ietf_codes) - languages = [ - lang_code_and_name - for lang_code_and_name in resource_lookup.lang_codes_and_names_having_usfm() - if lang_code_and_name[0] in ietf_codes - ] + ietf_codes_for_docs_with_fourth_column = [] + for ietf_code in ietf_codes: + doc = Document(f"{stet_dir}/stet_{ietf_code}.docx") + for table in doc.tables: + for row in table.rows: + # If 4th column exists, get bolded words from it + if len(row.cells) > 3 and row.cells[3].text: + if ietf_code not in ietf_codes_for_docs_with_fourth_column: + ietf_codes_for_docs_with_fourth_column.append(ietf_code) + languages = [] + for lang_code_and_name in resource_lookup.lang_codes_and_names_having_usfm(): + if is_production: + if lang_code_and_name[0] in ietf_codes_for_docs_with_fourth_column: + languages.append(lang_code_and_name) + else: + if lang_code_and_name[0] in ietf_codes: + languages.append(lang_code_and_name) # logger.debug("source languages: %s", languages) return languages diff --git a/frontend/src/routes/stet/source_languages/+page.svelte b/frontend/src/routes/stet/source_languages/+page.svelte index db9799c2..dbccecbf 100644 --- a/frontend/src/routes/stet/source_languages/+page.svelte +++ b/frontend/src/routes/stet/source_languages/+page.svelte @@ -2,7 +2,8 @@ import { onMount } from 'svelte' import { PUBLIC_STET_SOURCE_LANG_CODES_NAMES_URL, - PUBLIC_TAILWIND_SM_MIN_WIDTH + PUBLIC_TAILWIND_SM_MIN_WIDTH, + PUBLIC_PRODUCTION_DOMAIN } from '$env/static/public' import { env } from '$env/dynamic/public' import WizardBasketModal from '$lib/WizardBasketModal.svelte' @@ -21,6 +22,9 @@ } from '$lib/stet/stores/LanguagesStore' import { getCode, getName } from '$lib/stet/utils' + const isProduction = () => + window.location.hostname.includes(PUBLIC_PRODUCTION_DOMAIN) ? true : false + let showGatewayLanguages = true // For use by Mobile UI @@ -31,7 +35,10 @@ apiRootUrl: string = env.PUBLIC_BACKEND_API_URL, langCodesAndNamesUrl: string = PUBLIC_STET_SOURCE_LANG_CODES_NAMES_URL ): Promise> { - const response = await fetch(`${apiRootUrl}${langCodesAndNamesUrl}`) + console.log('frontend sees hostname:', window.location.hostname) + const response = await fetch(`${apiRootUrl}${langCodesAndNamesUrl}`, { + headers: { 'X-Is-Production': isProduction() ? 'true' : 'false' } + }) if (!response.ok) { console.log(`Error: ${response.statusText}`) throw new Error(response.statusText) diff --git a/frontend/tests/e2e/stet_test.ts b/frontend/tests/e2e/stet_test.ts index 12d7edd4..247efbf5 100644 --- a/frontend/tests/e2e/stet_test.ts +++ b/frontend/tests/e2e/stet_test.ts @@ -72,6 +72,56 @@ test.describe('Desktop Tests', () => { await page.getByRole('button', { name: 'Next' }).click() await page.getByRole('button', { name: 'Generate File' }).click() }) + + test('stet passages available when not in production', async ({ page }) => { + // log every request to the backend endpoint + page.on('request', (req) => { + if (req.url().includes('/stet/source_languages')) { + console.log('Request headers:', req.headers()) + } + }) + // add console output to test output + page.on('console', (msg) => { + console.log('PAGE LOG:', msg.text()) + }) + // mock header for test + await page.route('**/stet/source_languages', (route) => { + const headers = { + ...route.request().headers(), + 'x-is-production': 'false' + } + route.continue({ headers }) + }) + await page.goto('http://localhost:8001/stet') + await expect(page.getByText('English')).toBeVisible({ timeout: 32_000 }) + await expect(page.getByText('Tok Pisin')).toBeVisible({ timeout: 32_000 }) + }) + + test('stet input docs available in production should be limited to those with 4th column', async ({ + page + }) => { + // log every request to the backend endpoint + page.on('request', (req) => { + if (req.url().includes('/stet/source_languages')) { + console.log('Request headers:', req.headers()) + } + }) + // add console output to test output + page.on('console', (msg) => { + console.log('PAGE LOG:', msg.text()) + }) + // mock header for test + await page.route('**/stet/source_languages', (route) => { + const headers = { + ...route.request().headers(), + 'x-is-production': 'true' + } + route.continue({ headers }) + }) + await page.goto('http://localhost:8001/stet') + await expect(page.getByText('English')).toBeVisible({ timeout: 32_000 }) + await expect(page.getByText('Tok Pisin')).not.toBeVisible({ timeout: 32_000 }) + }) }) // Separate group for mobile tests @@ -107,6 +157,6 @@ test.describe('Mobile Tests', () => { await page.getByText('Abé').click() await page.getByRole('button').nth(1).click() await page.getByRole('button').first().click() - await expect(page.getByLabel('Abé aba')).toBeChecked({ timeout: 1200000 }) + // await expect(page.getByLabel('Abé aba')).toBeChecked({ timeout: 1200000 }) }) }) From ea0aaf34595b10a24b3b8d1310c1addb99947fc6 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Sat, 20 Sep 2025 19:34:49 -0700 Subject: [PATCH 206/208] Remove test that is not needed anymore --- frontend/tests/e2e/test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/frontend/tests/e2e/test.ts b/frontend/tests/e2e/test.ts index d812831a..0858893e 100644 --- a/frontend/tests/e2e/test.ts +++ b/frontend/tests/e2e/test.ts @@ -172,21 +172,6 @@ test('optional settings', async ({ page }) => { ) }) -test.skip('aba philemon', async ({ page }) => { - await page.goto('http://localhost:8001/') - await page.getByRole('button', { name: 'Heart' }).click() - await page.getByText('Abé aba').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Philémon').click({ timeout: 580000 }) - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('Regular').click() - await page.getByRole('button', { name: 'Next' }).click() - await page.getByText('PDF').click() - await page.getByRole('button', { name: 'Generate File' }).click() - await expect(page.locator('body')).toContainText('Philémon') - await expect(page.locator('body')).toContainText('Regular (aba)') -}) - test('book name correction happened for pt-br, 1co', async ({ page }) => { await page.goto('http://localhost:8001/') await page.getByText('Português Brasileiro (').click() From 9f9f667a92547a861cc5057fce1ed5da061c89f4 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Sat, 20 Sep 2025 19:35:24 -0700 Subject: [PATCH 207/208] Change a conditional to a different style --- frontend/tests/e2e/stet_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/tests/e2e/stet_test.ts b/frontend/tests/e2e/stet_test.ts index 247efbf5..488047b5 100644 --- a/frontend/tests/e2e/stet_test.ts +++ b/frontend/tests/e2e/stet_test.ts @@ -36,7 +36,7 @@ test.describe('Desktop Tests', () => { await page.getByPlaceholder('Search Heart Languages').fill('sxw') await page.getByText('Sègbé').click() await page.getByRole('button', { name: 'Next' }).click() - await expect(page.locator('body')).toContainText('Sègbé(sxw-x-segbe)') + await expect(page.getByText('Sègbé(sxw-x-segbe)')).toBeVisible() }) test('next then back and edit', async ({ page }) => { From d30cce62691e2fd6c3aa99d4be3aeb33227bd2f9 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Thu, 25 Sep 2025 11:19:30 -0700 Subject: [PATCH 208/208] Add Swahili STET input doc and locale info --- backend/doc/domain/resource_lookup.py | 3 +++ backend/stet/data/stet_sw.docx | Bin 0 -> 91413 bytes backend/stet/domain/strings.py | 4 ++++ 3 files changed, 7 insertions(+) create mode 100644 backend/stet/data/stet_sw.docx diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 6f7d75c1..20c42a46 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -182,6 +182,9 @@ ("es-419", "I juan"): "1 Juan", ("fr", "Ephésiens"): "Éphésiens", ("pt-br", "1 Corintios"): "1 Coríntios", + ("sw", "Matendo ya mitume"): "Matendo ya Mitume", + ("sw", "Luke"): "Luka", + ("sw", "Waraka wa yakobo"): "Yakobo", } # List of languages which do not have USFM available for any books. We use this diff --git a/backend/stet/data/stet_sw.docx b/backend/stet/data/stet_sw.docx new file mode 100644 index 0000000000000000000000000000000000000000..5ee9bc2b7a53624fe7726aacc8c05b31cb54dc58 GIT binary patch literal 91413 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 CDhR>= literal 0 HcmV?d00001 diff --git a/backend/stet/domain/strings.py b/backend/stet/domain/strings.py index adb9a1ae..28ddbe95 100644 --- a/backend/stet/domain/strings.py +++ b/backend/stet/domain/strings.py @@ -3,6 +3,7 @@ "es-419": "Herramienta de Evaluación de Términos Espirituales (STET)", "fr": "Évaluation des Termes Spirituels (STET)", "pt-br": "Ferramenta de Avaliação de Termos Espirituais (STET)", + "sw": "Chombo cha Kutathmini Maneno ya Kiroho (STET)", "tpi": "Tul bilong skelim ol spirit tok bilong buk trenslesen (STET)", } @@ -11,6 +12,7 @@ "es-419": "Generado el", "fr": "Généré le", "pt-br": "Gerado em", + "sw": "Imetolewa tarehe", "tpi": "Wok i bin kamap long", } @@ -19,6 +21,7 @@ "es-419": "%d/%m/%Y %H:%M:%S", "fr": "%d/%m/%Y %H:%M:%S", "pt-br": "%d/%m/%Y %H:%M:%S", + "sw": "%d/%m/%Y %H:%M:%S", "tpi": "%d/%m/%Y %H:%M:%S", } @@ -27,5 +30,6 @@ "es-419": ("Fuente", "Idioma Materna", "Estado", "OK"), "fr": ("Référence de source", "Référence Cible", "Statut", "OK"), "pt-br": ("Referência de Origem", "Referência de Destino", "Status", "OK"), + "sw": ("Marejeo Chanzo", "Marejeo Lengwa", "Hali", "OK"), "tpi": ("Narapela baibel ves", "Tokples ves", "Sek", "OK"), }