From a6ee264e480aa1ed615d3e978903b7c58a56aab2 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Sat, 24 Jan 2026 12:59:11 -0800 Subject: [PATCH 01/44] Fix enable disable of passages in passages basket and document ...based on availability of passages for language chosen. --- backend/doc/domain/resource_lookup.py | 13 +- backend/doc/utils/file_utils.py | 6 +- backend/passages/domain/document_generator.py | 70 ++++---- backend/passages/domain/model.py | 9 +- .../src/lib/passages/PassagesBasket.svelte | 10 +- frontend/src/lib/passages/models/passage.ts | 7 +- frontend/src/lib/passages/utils.ts | 19 +- .../src/routes/passages/passages/+page.svelte | 5 +- .../passages/passages/AddNTComponent.svelte | 76 ++++---- .../passages/passages/AddOTComponent.svelte | 170 +++++++++++++----- .../passages/BibleReferenceSelector.svelte | 7 +- .../passages/settings/GenerateDocument.svelte | 23 +-- 12 files changed, 270 insertions(+), 145 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 20878848..0e5cfc37 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -1212,11 +1212,14 @@ def nt_survey_rg_passages( resource_dir: str = settings.EN_RG_DIR, ) -> list[BibleReference]: """ - >>> from doc.domain import resource_lookup - >>> ();rg_books = resource_lookup.nt_survey_rg_passages() ;() # doctest: +ELLIPSIS - (...) - >>> rg_books[0] - BibleReference(book_code='mat', book_name='Matthew', start_chapter=2, start_chapter_verse_ref='1-12', end_chapter=None, end_chapter_verse_ref=None) + Returns the list of all NT RG passages from the docx_file_path, but with + book names localized for language chosen. + + >>> from doc.domain import resource_lookup + >>> ();rg_books = resource_lookup.nt_survey_rg_passages() ;() # doctest: +ELLIPSIS + (...) + >>> rg_books[0] + BibleReference(book_code='mat', book_name='Matthew', start_chapter=2, start_chapter_verse_ref='1-12', end_chapter=None, end_chapter_verse_ref=None) """ path = join(resource_dir, docx_file_path) rg_books = get_rg_books( diff --git a/backend/doc/utils/file_utils.py b/backend/doc/utils/file_utils.py index 3c39b72c..ecaeb882 100644 --- a/backend/doc/utils/file_utils.py +++ b/backend/doc/utils/file_utils.py @@ -154,7 +154,9 @@ def epub_filepath( def docx_filepath( - document_request_key: str, output_dir: str = settings.DOCUMENT_OUTPUT_DIR + document_request_key: str, + prefix: str = "", + output_dir: str = settings.DOCUMENT_OUTPUT_DIR, ) -> str: """Given document_request_key, return the docx output file path.""" - return join(output_dir, "{}.docx".format(document_request_key)) + return join(output_dir, "{}{}.docx".format(prefix, document_request_key)) diff --git a/backend/passages/domain/document_generator.py b/backend/passages/domain/document_generator.py index 6981955f..efa097ff 100644 --- a/backend/passages/domain/document_generator.py +++ b/backend/passages/domain/document_generator.py @@ -21,10 +21,13 @@ from doc.utils.text_utils import maybe_correct_book_name from docx import Document from docx.oxml import OxmlElement, parse_xml -from docx.shared import Inches +from docx.shared import Inches, RGBColor from htmldocx import HtmlToDocx # type: ignore -from passages.domain.model import Passage, BibleReference as PassageReference +from passages.domain.model import ( + Passage, + BibleReferenceWithAvailability as PassageReference, +) from passages.domain.parser import verse_text_html from passages.domain.stet_verse_list_parser import BOOK_INDEX, parse_bible_blocks from passages.utils.docx_utils import add_footer, add_header @@ -58,14 +61,12 @@ def generate_docx_document( str, str ] = settings.RESOURCE_TYPE_CODES_AND_NAMES, ) -> str: - """ - Generate the scriptural terms evaluation document. - - >>> from passages.domain.document_generator import generate_docx_document - >>> generate_docx_document("en", list[PassageReferenceDto(lang_code="en", book_code="mat", book_name="Matthew", chapter_num=1, verse_reference="3-6"), PassageReferenceDto(lang_code="en", book_code="mat", book_name="Matthew", chapter_num=1, verse_reference="9-10"), PassageReferenceDto(lang_code="en", book_code="mat", book_name="Matthew", chapter_num=1, verse_reference="15")]) - """ + """Generate the content for the Passages document""" book_codes = list( - {passage_ref_dto.book_code for passage_ref_dto in passage_reference_dtos} + { + passage_ref_dto.reference.book_code + for passage_ref_dto in passage_reference_dtos + } ) resource_types_ = resource_types(lang_code, ",".join(book_codes)) resource_types_codes = list( @@ -116,11 +117,12 @@ def generate_docx_document( current_task.update_state(state="Assembling content") passages = [] for passage_ref_dto in passage_reference_dtos: + reference = passage_ref_dto.reference selected_usfm_books = [ usfm_book_ for usfm_book_ in usfm_books if usfm_book_.lang_code == lang_code - and usfm_book_.book_code == passage_ref_dto.book_code + and usfm_book_.book_code == reference.book_code and usfm_book_.resource_type_name == resource_type_codes_and_names[usfm_resource_type] ] @@ -129,33 +131,29 @@ def generate_docx_document( if selected_usfm_books: selected_usfm_book = selected_usfm_books[0] if selected_usfm_book: - verse_text_html_ = verse_text_html(passage_ref_dto, selected_usfm_book) + verse_text_html_ = verse_text_html(reference, selected_usfm_book) else: verse_text_html_ = "" non_book_name_portion_of_reference = "" if ( - passage_ref_dto.end_chapter - and passage_ref_dto.end_chapter > 0 - and passage_ref_dto.end_chapter_verse_ref + reference.end_chapter + and reference.end_chapter > 0 + and reference.end_chapter_verse_ref ): - non_book_name_portion_of_reference = f"{passage_ref_dto.start_chapter}:{passage_ref_dto.start_chapter_verse_ref}-{passage_ref_dto.end_chapter}:{passage_ref_dto.end_chapter_verse_ref}" + non_book_name_portion_of_reference = f"{reference.start_chapter}:{reference.start_chapter_verse_ref}-{reference.end_chapter}:{reference.end_chapter_verse_ref}" else: - non_book_name_portion_of_reference = f"{passage_ref_dto.start_chapter}:{passage_ref_dto.start_chapter_verse_ref}" - # NOTE We want the document to show references even if there is no - # content for it. - # localized_reference = ( - # f"{selected_usfm_book.national_book_name} {non_book_name_portion_of_reference}" - # if selected_usfm_book and non_book_name_portion_of_reference - # else passage_ref_dto.start_chapter_verse_ref - # ) + non_book_name_portion_of_reference = ( + f"{reference.start_chapter}:{reference.start_chapter_verse_ref}" + ) localized_reference = ( - f"{passage_ref_dto.book_name} {non_book_name_portion_of_reference}" - if non_book_name_portion_of_reference - else passage_ref_dto.start_chapter_verse_ref + f"{selected_usfm_book.national_book_name} {non_book_name_portion_of_reference}" + if selected_usfm_book and non_book_name_portion_of_reference + else f"{reference.book_name} {non_book_name_portion_of_reference}" ) passage = Passage( bible_reference=localized_reference, passage_text=verse_text_html_, + is_available=passage_ref_dto.is_available, ) passages.append(passage) current_task.update_state(state="Converting to Docx") @@ -170,6 +168,7 @@ def generate_docx( lang_name: str, show_notes_column: bool = False, # TODO make a UI option, for now default to False ) -> None: + """Generate the Passages output document in docx format""" TOTAL_WIDTH = Inches(6.0) doc = Document() html_to_docx = HtmlToDocx() @@ -197,10 +196,16 @@ def generate_docx( tcPr.append(tcW) # Fill left (or only) cell cell_left = table.cell(0, 0) - html_to_docx.add_html_to_document( - passage_dto.bible_reference, - cell_left, - ) + # Add bible reference with color depending on availability + p = cell_left.add_paragraph() # create a fresh paragraph for control + run = p.add_run(passage_dto.bible_reference) + # run.bold = True # optional – if you want it emphasized + # run.font.size = Pt(10) # optional + if passage_dto.is_available: + run.font.color.rgb = RGBColor(102, 118, 139) # ≈ #66768B + else: + run.font.color.rgb = RGBColor(176, 184, 195) # ≈ #B0B8C3 + # Then add the passage text (still using html_to_docx if it has other formatting) html_to_docx.add_html_to_document( passage_dto.passage_text, cell_left, @@ -262,7 +267,7 @@ def document_request_key( translation_table = str.maketrans(":;,-", "____") passages_key = underscore.join( [ - f"{passage_reference.book_code}_{passage_reference.start_chapter}_{passage_reference.start_chapter_verse_ref.translate(translation_table)}" + f"{passage_reference.reference.book_code}_{passage_reference.reference.start_chapter}_{passage_reference.reference.start_chapter_verse_ref.translate(translation_table)}" for passage_reference in passage_reference_dtos ] ) @@ -299,7 +304,8 @@ def generate_passages_docx_document( email_address, ) document_request_key_ = document_request_key(lang_code, passage_reference_dtos) - docx_filepath_ = f"{docx_filepath_prefix}{docx_filepath(document_request_key_)}" + docx_filepath_ = f"{docx_filepath(document_request_key_, docx_filepath_prefix)}" + logger.debug("docx_filepath_: %s", docx_filepath_) if file_needs_update(docx_filepath_): generate_docx_document( lang_code, diff --git a/backend/passages/domain/model.py b/backend/passages/domain/model.py index 13dce47a..07dbf99e 100644 --- a/backend/passages/domain/model.py +++ b/backend/passages/domain/model.py @@ -15,15 +15,22 @@ class BibleReference(BaseModel): end_chapter_verse_ref: Optional[str] +@final +class BibleReferenceWithAvailability(BaseModel): + reference: BibleReference + is_available: bool + + @final class Passage(NamedTuple): passage_text: str # HTML of passage bible_reference: str + is_available: bool @final class PassagesDocumentRequest(BaseModel): lang_code: str lang_name: str - bible_references: list[BibleReference] + bible_references: list[BibleReferenceWithAvailability] email_address: Optional[EmailStr] diff --git a/frontend/src/lib/passages/PassagesBasket.svelte b/frontend/src/lib/passages/PassagesBasket.svelte index a5ac3ee4..8dcfebeb 100644 --- a/frontend/src/lib/passages/PassagesBasket.svelte +++ b/frontend/src/lib/passages/PassagesBasket.svelte @@ -4,7 +4,7 @@ import { langCodeAndNameStore } from '$lib/passages/stores/LanguagesStore' import { passagesStore, filteredPassagesStore } from '$lib/passages/stores/PassagesStore' import type { BibleReference } from '$lib/passages/models' - import { passagesRegExp } from '$lib/passages/utils' + import { isAvailable, passagesRegExp } from '$lib/passages/utils' import BookIcon from '$lib/BookIcon.svelte' import EditIcon from '$lib/EditIcon.svelte' import CloseIcon from '$lib/CloseIcon.svelte' @@ -39,13 +39,7 @@ $: shownPassages = sortedPassages?.slice(0, size) $: hiddenPassages = sortedPassages?.slice(size) - $: idsOfAvailablePassages = new Set( - ($filteredPassagesStore ?? []).map((p: BibleReference) => p.id) - ) - function isAvailable(passage: BibleReference): boolean { - return idsOfAvailablePassages.has(passage.id) - } {#if passagesRegExp.test($page.url.pathname)} @@ -153,7 +147,7 @@ {#if isAvailable(passage)} {/if} diff --git a/frontend/src/lib/passages/models/passage.ts b/frontend/src/lib/passages/models/passage.ts index f566c5cd..b94d27f1 100644 --- a/frontend/src/lib/passages/models/passage.ts +++ b/frontend/src/lib/passages/models/passage.ts @@ -1,7 +1,7 @@ export type PassagesDocumentRequest = { langCode: string langName: string - bibleReferences: Array + bibleReferences: Array // Now uses the wrapper emailAddress: string | null } @@ -15,3 +15,8 @@ export type BibleReference = { endChapter?: number | null endChapterVerseRef?: string | null } + +export type BibleReferenceWithAvailability = { + reference: BibleReference // The original reference object + isAvailable: boolean // Flag indicating if this reference is available (e.g., based on prior checks) +} diff --git a/frontend/src/lib/passages/utils.ts b/frontend/src/lib/passages/utils.ts index 575798e9..a2e0d9bd 100644 --- a/frontend/src/lib/passages/utils.ts +++ b/frontend/src/lib/passages/utils.ts @@ -1,3 +1,4 @@ +import { get } from 'svelte/store' import { browser } from '$app/environment' import { goto } from '$app/navigation' import { @@ -6,8 +7,10 @@ import { langCodeAndNameStore } from '$lib/passages/stores/LanguagesStore' import { documentReadyStore, errorStore } from '$lib/passages/stores/NotificationStore' +import { filteredPassagesStore } from '$lib/passages/stores/PassagesStore' import { documentRequestKeyStore } from '$lib/passages/stores/SettingsStore' import type { PassagesDocumentRequest } from '$lib/passages/models' +import type { BibleReference } from '$lib/passages/models' type StoreGroup = 'language' | 'settings' | 'notifications' @@ -45,10 +48,14 @@ export function routeToPage(url: string): void { } } -// Function to omit the 'id' from passageReferences just before stringifying -export function omitIdFromPassageReferences(documentRequest: PassagesDocumentRequest) { - return { - ...documentRequest, - bibleReferences: documentRequest.bibleReferences.map(({ id, ...rest }) => rest) // Omit 'id' - } + +export function isAvailable(passage: BibleReference): boolean { + const currentFiltered = get(filteredPassagesStore) + return currentFiltered.some( + (ref: BibleReference) => + ref.langCode === passage.langCode && + ref.bookCode === passage.bookCode && + ref.startChapter === passage.startChapter && + ref.startChapterVerseRef === passage.startChapterVerseRef + ) } diff --git a/frontend/src/routes/passages/passages/+page.svelte b/frontend/src/routes/passages/passages/+page.svelte index f997ca7a..42593779 100644 --- a/frontend/src/routes/passages/passages/+page.svelte +++ b/frontend/src/routes/passages/passages/+page.svelte @@ -1,5 +1,4 @@ +{#if loading} + +{/if} {#if showNT}
+ import { onMount } from 'svelte' + import ProgressIndicator from '$lib/ProgressIndicator.svelte' import { PUBLIC_OT_SURVEY_RG1_PASSAGES_URL, PUBLIC_OT_SURVEY_RG2_PASSAGES_URL, @@ -8,11 +10,15 @@ import { env } from '$env/dynamic/public' import type { BibleReference } from './model' import { langCodeAndNameStore } from '$lib/passages/stores/LanguagesStore' - import { addBibleReference, removeBibleReference } from '$lib/passages/stores/PassagesStore' + import { + addBibleReference, + addFilteredBibleReference, + removeBibleReference + } from '$lib/passages/stores/PassagesStore' import { bookRange } from '$lib/bible-books' import type { BookKey } from '$lib/bible-books' - export let loading: boolean + let loading: boolean = false export let checkIcon: string export let bookCodesAndNames: [string, string][] let isLoadingOTSurveyRG1 = false @@ -23,6 +29,18 @@ let otSurveyRG2SuccessMessage: string = '' let otSurveyRG3SuccessMessage: string = '' let otSurveyRG4SuccessMessage: string = '' + let rg1BibleReferences: Array = [] + let availableRg1BibleReferences: Array = [] + let showRG1: boolean = false + let rg2BibleReferences: Array = [] + let availableRg2BibleReferences: Array = [] + let showRG2: boolean = false + let rg3BibleReferences: Array = [] + let availableRg3BibleReferences: Array = [] + let showRG3: boolean = false + let rg4BibleReferences: Array = [] + let availableRg4BibleReferences: Array = [] + let showRG4: boolean = false async function getOTSurveyRG1Passages( langCode: string, @@ -37,9 +55,7 @@ console.error(response.statusText) throw new Error(response.statusText) } - return bibleReferences.filter((ref) => - bookCodesAndNames.some(([code]) => code === ref.book_code) - ) + return bibleReferences } async function getOTSurveyRG2Passages( @@ -55,9 +71,7 @@ console.error(response.statusText) throw new Error(response.statusText) } - return bibleReferences.filter((ref) => - bookCodesAndNames.some(([code]) => code === ref.book_code) - ) + return bibleReferences } async function getOTSurveyRG3Passages( @@ -73,9 +87,7 @@ console.error(response.statusText) throw new Error(response.statusText) } - return bibleReferences.filter((ref) => - bookCodesAndNames.some(([code]) => code === ref.book_code) - ) + return bibleReferences } async function getOTSurveyRG4Passages( @@ -91,11 +103,112 @@ console.error(response.statusText) throw new Error(response.statusText) } - return bibleReferences.filter((ref) => - bookCodesAndNames.some(([code]) => code === ref.book_code) - ) + return bibleReferences } + onMount(async () => { + loading = true + const langCode = $langCodeAndNameStore.split(',')[0] + try { + // Get all the OT RG 1 passages + rg1BibleReferences = await getOTSurveyRG1Passages(langCode) + // Filter down to the OT RG 1 passages available in this language + availableRg1BibleReferences = rg1BibleReferences.filter((ref) => + bookCodesAndNames.some(([code]) => code === ref.book_code) + ) + // Add available bible references to filteredPassagesStore for + // later reference in PassagesBasket + for (const bibleRef of availableRg1BibleReferences) { + addFilteredBibleReference( + langCode, + bibleRef.book_code, + bibleRef.book_name, + Number(bibleRef.start_chapter), + bibleRef.start_chapter_verse_ref, + Number(bibleRef.end_chapter), + bibleRef.end_chapter_verse_ref + ) + } + // Set flag indicating if this language provides any of the OT + // RG 1 survey passages. + showRG1 = availableRg1BibleReferences.length > 0 + + // Get all the OT RG 2 passages + rg2BibleReferences = await getOTSurveyRG2Passages(langCode) + // Filter down to the OT RG 2 passages available in this language + availableRg2BibleReferences = rg2BibleReferences.filter((ref) => + bookCodesAndNames.some(([code]) => code === ref.book_code) + ) + // Add available bible references to filteredPassagesStore for + // later reference in PassagesBasket + for (const bibleRef of availableRg2BibleReferences) { + addFilteredBibleReference( + langCode, + bibleRef.book_code, + bibleRef.book_name, + Number(bibleRef.start_chapter), + bibleRef.start_chapter_verse_ref, + Number(bibleRef.end_chapter), + bibleRef.end_chapter_verse_ref + ) + } + // Set flag indicating if this language provides any of the 0T + // RG 2 survey passages. + showRG2 = availableRg2BibleReferences.length > 0 + + // Get all the OT RG 3 passages + rg3BibleReferences = await getOTSurveyRG3Passages(langCode) + // Filter down to the OT RG 3 passages available in this language + availableRg3BibleReferences = rg3BibleReferences.filter((ref) => + bookCodesAndNames.some(([code]) => code === ref.book_code) + ) + // Add available bible references to filteredPassagesStore for + // later reference in PassagesBasket + for (const bibleRef of availableRg3BibleReferences) { + addFilteredBibleReference( + langCode, + bibleRef.book_code, + bibleRef.book_name, + Number(bibleRef.start_chapter), + bibleRef.start_chapter_verse_ref, + Number(bibleRef.end_chapter), + bibleRef.end_chapter_verse_ref + ) + } + // Set flag indicating if this language provides any of the 0T + // RG 3 survey passages. + showRG3 = availableRg3BibleReferences.length > 0 + + // Get all the OT RG 4 passages + rg4BibleReferences = await getOTSurveyRG4Passages(langCode) + // Filter down to the OT RG 4 passages available in this language + availableRg4BibleReferences = rg4BibleReferences.filter((ref) => + bookCodesAndNames.some(([code]) => code === ref.book_code) + ) + // Add available bible references to filteredPassagesStore for + // later reference in PassagesBasket + for (const bibleRef of availableRg4BibleReferences) { + addFilteredBibleReference( + langCode, + bibleRef.book_code, + bibleRef.book_name, + Number(bibleRef.start_chapter), + bibleRef.start_chapter_verse_ref, + Number(bibleRef.end_chapter), + bibleRef.end_chapter_verse_ref + ) + } + // Set flag indicating if this language provides any of the 0T + // RG 4 survey passages. + showRG4 = availableRg4BibleReferences.length > 0 + } catch (error) { + console.error('Failed to load NT Survey RG passages:', error) + } finally { + console.log('NT Survey RG passages loaded successfully') + } + loading = false + }) + export async function addOTSurveyRG1Passages() { try { const langCode = $langCodeAndNameStore.split(',')[0] @@ -269,7 +382,6 @@ } async function handleAddOTSurveyRG1PassagesClick() { - loading = true isLoadingOTSurveyRG1 = true try { await addOTSurveyRG1Passages() @@ -277,13 +389,11 @@ } catch (error) { console.error('Error:', error) } finally { - loading = false isLoadingOTSurveyRG1 = false } } async function handleAddOTSurveyRG2PassagesClick() { - loading = true isLoadingOTSurveyRG2 = true try { await addOTSurveyRG2Passages() @@ -291,13 +401,11 @@ } catch (error) { console.error('Error:', error) } finally { - loading = false isLoadingOTSurveyRG2 = false } } async function handleAddOTSurveyRG3PassagesClick() { - loading = true isLoadingOTSurveyRG3 = true try { await addOTSurveyRG3Passages() @@ -305,13 +413,11 @@ } catch (error) { console.error('Error:', error) } finally { - loading = false isLoadingOTSurveyRG3 = false } } async function handleAddOTSurveyRG4PassagesClick() { - loading = true isLoadingOTSurveyRG4 = true try { await addOTSurveyRG4Passages() @@ -319,13 +425,11 @@ } catch (error) { console.error('Error:', error) } finally { - loading = false isLoadingOTSurveyRG4 = false } } async function handleRemoveOTSurveyRG1PassagesClick() { - loading = true isLoadingOTSurveyRG1 = true try { await removeOTSurveyRG1Passages() @@ -333,13 +437,11 @@ } catch (error) { console.error('Error:', error) } finally { - loading = false isLoadingOTSurveyRG1 = false } } async function handleRemoveOTSurveyRG2PassagesClick() { - loading = true isLoadingOTSurveyRG2 = true try { await removeOTSurveyRG2Passages() @@ -347,13 +449,11 @@ } catch (error) { console.error('Error:', error) } finally { - loading = false isLoadingOTSurveyRG2 = false } } async function handleRemoveOTSurveyRG3PassagesClick() { - loading = true isLoadingOTSurveyRG3 = true try { await removeOTSurveyRG3Passages() @@ -361,13 +461,11 @@ } catch (error) { console.error('Error:', error) } finally { - loading = false isLoadingOTSurveyRG3 = false } } async function handleRemoveOTSurveyRG4PassagesClick() { - loading = true isLoadingOTSurveyRG4 = true try { await removeOTSurveyRG4Passages() @@ -375,7 +473,6 @@ } catch (error) { console.error('Error:', error) } finally { - loading = false isLoadingOTSurveyRG4 = false } } @@ -473,18 +570,11 @@ (showRG3 ? otSurveyRG3Checked : true) && (showRG4 ? otSurveyRG4Checked : true) } - - const rg1 = bookRange('gen', 'deu') - const rg2 = bookRange('jos', 'est') - const rg3 = bookRange('job', 'sng') - const rg4 = bookRange('isa', 'mal') - - let showRG1 = bookCodesAndNames.some(([code]) => rg1.includes(code as BookKey)) - let showRG2 = bookCodesAndNames.some(([code]) => rg2.includes(code as BookKey)) - let showRG3 = bookCodesAndNames.some(([code]) => rg3.includes(code as BookKey)) - let showRG4 = bookCodesAndNames.some(([code]) => rg4.includes(code as BookKey)) +{#if loading} + +{/if} {#if [showRG1, showRG2, showRG3, showRG4].filter(Boolean).length > 1}
- - + +
diff --git a/frontend/src/routes/passages/settings/GenerateDocument.svelte b/frontend/src/routes/passages/settings/GenerateDocument.svelte index ebb4badc..c91c6359 100644 --- a/frontend/src/routes/passages/settings/GenerateDocument.svelte +++ b/frontend/src/routes/passages/settings/GenerateDocument.svelte @@ -10,15 +10,14 @@ settingsUpdatedStore } from '$lib/passages/stores/SettingsStore' import { taskIdStore, taskStateStore } from '$lib/passages/stores/TaskStore' - import { getCode, getName } from '$lib/passages/utils' + import { getCode, getName, isAvailable } from '$lib/passages/utils' import LogRocket from 'logrocket' import TaskStatus from './TaskStatus.svelte' import type { PassagesDocumentRequest } from '$lib/passages/models/passage' import { toSnakeCase } from '$lib/camel-to-snake-case-util' - import { omitIdFromPassageReferences } from '$lib/passages/utils' import ErrorAlertIcon from '$lib/ErrorAlertIcon.svelte' import { bookCodes } from '$lib/bible-books' - import type { BibleReference } from '$lib/passages/models' + import type { BibleReference, BibleReferenceWithAvailability } from '$lib/passages/models' let apiRootUrl = env.PUBLIC_BACKEND_API_URL let fileServerUrl: string = env.PUBLIC_FILE_SERVER_URL @@ -67,14 +66,16 @@ const documentRequest: PassagesDocumentRequest = { langCode: getCode($langCodeAndNameStore), langName: getName($langCodeAndNameStore), - bibleReferences: sortedPassages, + bibleReferences: sortedPassages.map( + (ref) => + ({ + reference: ref, + isAvailable: isAvailable(ref) + }) as BibleReferenceWithAvailability + ), emailAddress: $emailStore } - const documentRequestWithoutIds = omitIdFromPassageReferences(documentRequest) - console.log( - 'document request: ', - JSON.stringify(toSnakeCase(documentRequestWithoutIds), null, 2) - ) + console.log('document request: ', JSON.stringify(toSnakeCase(documentRequest), null, 2)) $errorStore = null $documentReadyStore = false $documentRequestKeyStore = '' @@ -83,7 +84,7 @@ method: 'POST', headers: { 'Content-Type': 'application/json' }, // toSnakeCase for FastAPI (python) endpoint - body: JSON.stringify(toSnakeCase(documentRequestWithoutIds)) + body: JSON.stringify(toSnakeCase(documentRequest)) }) const data = await response.json() if (!response.ok) { @@ -144,7 +145,7 @@ // Reactively set download URLs of generated documents let docxDownloadUrl: string - $: docxDownloadUrl = `${fileServerUrl}/${$documentRequestKeyStore}.docx` + $: docxDownloadUrl = `${fileServerUrl}/passages_${$documentRequestKeyStore}.docx` function viewFromUrl(url: string) { console.log(`url: ${url}`) From 5ad05643135c73023581a95754b7677fa0dbdf10 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 30 Jan 2026 15:24:56 -0800 Subject: [PATCH 02/44] Fill in each missing localized book name with non localized book name This is a better option than using all non localized names if any one localized book name is missing. This way we localize book names as much as possible and where that is not possible, English names are used which serves to highlight content issues for later fix by content experts. --- backend/doc/domain/resource_lookup.py | 51 ++++++++++++++------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 0e5cfc37..84f978dc 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -821,24 +821,16 @@ def get_book_codes_for_lang_( manifest_name = book_codes_and_names_localized_from_manifest.get( code, "" ) - if not name and manifest_name: - book_codes_and_names_localized.append( - ( - code, - maybe_correct_book_name( - lang_code, normalize_localized_book_name(manifest_name) - ), - ) - ) - else: - book_codes_and_names_localized.append( - ( - code, - maybe_correct_book_name( - lang_code, normalize_localized_book_name(name) - ), - ) + chosen_name = name or manifest_name + book_codes_and_names_localized.append( + ( + code, + maybe_correct_book_name( + lang_code, + normalize_localized_book_name(chosen_name), + ), ) + ) elif ( use_localized_book_name and len(repo_components) > 2 @@ -862,7 +854,8 @@ def get_book_codes_for_lang_( ( repo_components[1], maybe_correct_book_name( - lang_code, normalize_localized_book_name(book_name_) + lang_code, + normalize_localized_book_name(book_name_), ), ) ) @@ -882,14 +875,22 @@ def get_book_codes_for_lang_( ) logger.debug("book_codes_and_names: %s", book_codes_and_names) logger.debug("book_codes_and_names_localized: %s", book_codes_and_names_localized) - if not book_codes_and_names_localized or any( - name == "" for _, name in book_codes_and_names_localized - ): - unique_values = unique_book_codes(book_codes_and_names) - else: - unique_values = unique_book_codes(book_codes_and_names_localized) + localized_map: dict[str, str] = { + code: name for code, name in book_codes_and_names_localized + } + non_localized_map: dict[str, str] = { + code: name for code, name in book_codes_and_names + } + merged: list[tuple[str, str]] = [] + for code in set(localized_map) | set(non_localized_map): + name = localized_map.get(code, "") + if not name: + name = non_localized_map.get(code, "") + merged.append((code, name)) + unique_values = unique_book_codes(merged) return sorted( - unique_values, key=lambda book_code_and_name: book_id_map[book_code_and_name[0]] + unique_values, + key=lambda book_code_and_name: book_id_map[book_code_and_name[0]], ) From 4aefa1057599ae90854acf17aa2928a841107213 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 30 Jan 2026 15:26:45 -0800 Subject: [PATCH 03/44] Better naming of local vars --- backend/passages/domain/stet_verse_list_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/passages/domain/stet_verse_list_parser.py b/backend/passages/domain/stet_verse_list_parser.py index bb983937..55d017c1 100644 --- a/backend/passages/domain/stet_verse_list_parser.py +++ b/backend/passages/domain/stet_verse_list_parser.py @@ -4,7 +4,7 @@ from doc.domain.bible_books import BOOK_NAMES -BOOK_INDEX = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) +BOOK_INDEX = dict((key, pos) for pos, key in enumerate(BOOK_NAMES.keys())) BOOK_VERSE_PATTERN = re.compile(r"(?:([1-3]?\s?[A-Z][a-z]+)\s)?(\d+:\d+)") HEADER_PATTERN = re.compile(r"^(\w+)\s+\(([\d,; ]+)\)") From b0e33ef70f3d771d7d94fc48ba4f2c53df959d61 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Fri, 30 Jan 2026 15:36:26 -0800 Subject: [PATCH 04/44] Passages: Handle up to two languages By request of PO --- backend/passages/domain/document_generator.py | 426 ++++++++++++------ backend/passages/domain/model.py | 11 +- backend/passages/entrypoints/routes.py | 15 +- backend/passages/utils/docx_utils.py | 16 +- frontend/src/lib/bible-books.ts | 8 - .../src/lib/passages/DesktopBreadcrumb.svelte | 4 +- .../src/lib/passages/LanguageBasket.svelte | 89 ++-- .../src/lib/passages/MobileBreadcrumb.svelte | 7 +- .../src/lib/passages/PassagesBasket.svelte | 32 +- .../src/lib/passages/WizardBreadcrumb.svelte | 1 - frontend/src/lib/passages/models/passage.ts | 6 +- .../src/lib/passages/stores/LanguagesStore.ts | 6 +- .../src/lib/passages/stores/PassagesStore.ts | 6 +- frontend/src/lib/passages/utils.ts | 11 +- .../src/routes/passages/language/+page.svelte | 3 +- .../language/DesktopLanguageDisplay.svelte | 24 +- .../language/MobileLanguageDisplay.svelte | 20 +- .../src/routes/passages/passages/+page.svelte | 41 +- .../passages/passages/AddNTComponent.svelte | 94 +++- .../passages/passages/AddOTComponent.svelte | 348 ++++++++++---- .../passages/AddPassageComponent.svelte | 42 +- .../passages/passages/AddSTETComponent.svelte | 11 +- .../passages/BibleReferenceSelector.svelte | 9 +- .../passages/settings/GenerateDocument.svelte | 21 +- 24 files changed, 838 insertions(+), 413 deletions(-) diff --git a/backend/passages/domain/document_generator.py b/backend/passages/domain/document_generator.py index efa097ff..4b559675 100644 --- a/backend/passages/domain/document_generator.py +++ b/backend/passages/domain/document_generator.py @@ -1,13 +1,13 @@ import json import time -from typing import Mapping, Sequence, TYPE_CHECKING +from typing import Mapping, Optional, Sequence, TYPE_CHECKING, cast from celery import current_task from doc.config import settings from doc.domain import worker 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 Attachment +from doc.domain.model import Attachment, USFMBook from doc.domain.parsing import split_chapter_into_verses, usfm_book_content from doc.domain.resource_lookup import ( book_codes_for_lang_from_usfm_only, @@ -20,21 +20,19 @@ from doc.utils.file_utils import docx_filepath, file_needs_update from doc.utils.text_utils import maybe_correct_book_name from docx import Document -from docx.oxml import OxmlElement, parse_xml +from docx.oxml import parse_xml from docx.shared import Inches, RGBColor - +from docx.table import _Cell, _Row from htmldocx import HtmlToDocx # type: ignore from passages.domain.model import ( Passage, - BibleReferenceWithAvailability as PassageReference, + BibleReferenceWithAvailability, ) from passages.domain.parser import verse_text_html from passages.domain.stet_verse_list_parser import BOOK_INDEX, parse_bible_blocks from passages.utils.docx_utils import add_footer, add_header from pydantic import Json -from docx.table import _Cell, _Row - if TYPE_CHECKING: from typing import TypeAlias @@ -48,64 +46,126 @@ logger = settings.logger(__name__) +def get_passages( + bible_references_with_availability: list[BibleReferenceWithAvailability], + usfm_resource_type: str, + usfm_books: list[USFMBook], + resource_type_codes_and_names: Mapping[ + str, str + ] = settings.RESOURCE_TYPE_CODES_AND_NAMES, +) -> list[Passage]: + passages = [] + for bible_reference_with_availability in bible_references_with_availability: + reference = bible_reference_with_availability.reference + # logger.debug("reference: %s", reference) + resource_type_name = resource_type_codes_and_names[usfm_resource_type] + selected_usfm_book = next( + ( + usfm_book_ + for usfm_book_ in usfm_books + if usfm_book_.book_code == reference.book_code + and usfm_book_.resource_type_name == resource_type_name + ), + None, + ) + verse_text_html_ = ( + verse_text_html(reference, selected_usfm_book) if selected_usfm_book else "" + ) + non_book_name_portion_of_reference = "" + if ( + reference.end_chapter + and reference.end_chapter > 0 + and reference.end_chapter_verse_ref + ): + non_book_name_portion_of_reference = f"{reference.start_chapter}:{reference.start_chapter_verse_ref}-{reference.end_chapter}:{reference.end_chapter_verse_ref}" + else: + non_book_name_portion_of_reference = ( + f"{reference.start_chapter}:{reference.start_chapter_verse_ref}" + ) + passage = Passage( + reference=reference, + localized_reference=f"{reference.book_name} {non_book_name_portion_of_reference}", + passage_text=verse_text_html_, + is_available=bible_reference_with_availability.is_available, + ) + passages.append(passage) + return passages + + def generate_docx_document( - lang_code: str, - lang_name: str, - passage_reference_dtos: list[PassageReference], + lang0_code: str, + lang0_name: str, + lang1_code: Optional[str], + lang1_name: Optional[str], + bible_references_with_availability: list[BibleReferenceWithAvailability], document_request_key_: str, docx_filepath_: str, working_dir: str = settings.WORKING_DIR, output_dir: str = settings.DOCUMENT_OUTPUT_DIR, usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, - resource_type_codes_and_names: Mapping[ - str, str - ] = settings.RESOURCE_TYPE_CODES_AND_NAMES, + book_index: dict[str, int] = BOOK_INDEX, ) -> str: """Generate the content for the Passages document""" - book_codes = list( + # Invariant: book names are already localized at this point + # logger.debug( + # "bible_references_with_availability: %s", bible_references_with_availability + # ) + # ----lang0: ----------------------------- + bible_references_with_availability_lang0: list[BibleReferenceWithAvailability] = [ + b + for b in bible_references_with_availability + if b.reference.lang_code == lang0_code + ] + # Invariant: book codes are only those that were available from USFM resources + book_codes_lang0 = list( + dict.fromkeys( + ref.reference.book_code for ref in bible_references_with_availability_lang0 + ) + ) + logger.debug( + "book_codes_lang0 prior to uniqification and sorting: %s", book_codes_lang0 + ) + resource_types_lang0 = resource_types(lang0_code, ",".join(book_codes_lang0)) + resource_types_codes_lang0 = list( { - passage_ref_dto.reference.book_code - for passage_ref_dto in passage_reference_dtos + lang_resource_type_tuple[0] + for lang_resource_type_tuple in resource_types_lang0 } ) - resource_types_ = resource_types(lang_code, ",".join(book_codes)) - resource_types_codes = list( - {lang_resource_type_tuple[0] for lang_resource_type_tuple in resource_types_} - ) - usfm_resource_types = list( + usfm_resource_types_lang0 = list( { resource_type_ - for resource_type_ in resource_types_codes + for resource_type_ in resource_types_codes_lang0 if resource_type_ in usfm_resource_types } ) - ulb_usfm_resource_types = list( + ulb_usfm_resource_types_lang0 = list( { usfm_resource_type_ - for usfm_resource_type_ in usfm_resource_types + for usfm_resource_type_ in usfm_resource_types_lang0 if "ulb" in usfm_resource_type_ } ) - usfm_books = [] - usfm_resource_type = "" - if ulb_usfm_resource_types: # Prefer ulb if available - usfm_resource_type = ulb_usfm_resource_types[0] - elif usfm_resource_types: - usfm_resource_type = usfm_resource_types[0] - if usfm_resource_type: + usfm_books_lang0 = [] + usfm_resource_type_lang0 = "" + if ulb_usfm_resource_types_lang0: # Prefer ulb if available + usfm_resource_type_lang0 = ulb_usfm_resource_types_lang0[0] + elif usfm_resource_types_lang0: + usfm_resource_type_lang0 = usfm_resource_types_lang0[0] + if usfm_resource_type_lang0: usfm_book = None - for book_code in book_codes: + for book_code in book_codes_lang0: current_task.update_state(state="Locating assets") - resource_lookup_dto_ = resource_lookup_dto( - lang_code, usfm_resource_type, book_code + resource_lookup_dto_lang0 = resource_lookup_dto( + lang0_code, usfm_resource_type_lang0, book_code ) - if resource_lookup_dto_ and resource_lookup_dto_.url: + if resource_lookup_dto_lang0 and resource_lookup_dto_lang0.url: current_task.update_state(state="Provisioning asset files") - resource_dir = prepare_resource_filepath(resource_lookup_dto_) - provision_asset_files(resource_lookup_dto_.url, resource_dir) + resource_dir = prepare_resource_filepath(resource_lookup_dto_lang0) + provision_asset_files(resource_lookup_dto_lang0.url, resource_dir) current_task.update_state(state="Parsing asset files") usfm_book = usfm_book_content( - resource_lookup_dto_, + resource_lookup_dto_lang0, resource_dir, False, ) @@ -113,110 +173,181 @@ def generate_docx_document( usfm_book.chapters[chapter_num_].verses = split_chapter_into_verses( chapter_ ) - usfm_books.append(usfm_book) - current_task.update_state(state="Assembling content") - passages = [] - for passage_ref_dto in passage_reference_dtos: - reference = passage_ref_dto.reference - selected_usfm_books = [ - usfm_book_ - for usfm_book_ in usfm_books - if usfm_book_.lang_code == lang_code - and usfm_book_.book_code == reference.book_code - and usfm_book_.resource_type_name - == resource_type_codes_and_names[usfm_resource_type] + usfm_books_lang0.append(usfm_book) + # ----lang1: ----------------------------- + bible_references_with_availability_lang1: list[BibleReferenceWithAvailability] = ( + [ + b + for b in bible_references_with_availability + if b.reference.lang_code == lang1_code ] - verse_text_html_ = "" - selected_usfm_book = None - if selected_usfm_books: - selected_usfm_book = selected_usfm_books[0] - if selected_usfm_book: - verse_text_html_ = verse_text_html(reference, selected_usfm_book) - else: - verse_text_html_ = "" - non_book_name_portion_of_reference = "" - if ( - reference.end_chapter - and reference.end_chapter > 0 - and reference.end_chapter_verse_ref - ): - non_book_name_portion_of_reference = f"{reference.start_chapter}:{reference.start_chapter_verse_ref}-{reference.end_chapter}:{reference.end_chapter_verse_ref}" - else: - non_book_name_portion_of_reference = ( - f"{reference.start_chapter}:{reference.start_chapter_verse_ref}" - ) - localized_reference = ( - f"{selected_usfm_book.national_book_name} {non_book_name_portion_of_reference}" - if selected_usfm_book and non_book_name_portion_of_reference - else f"{reference.book_name} {non_book_name_portion_of_reference}" + if lang1_code + else [] + ) + book_codes_lang1 = list( + dict.fromkeys( + ref.reference.book_code for ref in bible_references_with_availability_lang1 ) - passage = Passage( - bible_reference=localized_reference, - passage_text=verse_text_html_, - is_available=passage_ref_dto.is_available, + ) + logger.debug( + "book_codes_lang1 prior to uniqification and sorting: %s", book_codes_lang1 + ) + resource_types_lang1 = ( + resource_types(lang1_code, ",".join(book_codes_lang1)) if lang1_code else [] + ) + if lang1_code: + resource_types_lang1 = resource_types(lang1_code, ",".join(book_codes_lang1)) + resource_types_codes_lang1 = list( + { + lang_resource_type_tuple[0] + for lang_resource_type_tuple in resource_types_lang1 + } ) - passages.append(passage) + usfm_resource_types_lang1 = list( + { + resource_type_ + for resource_type_ in resource_types_codes_lang1 + if resource_type_ in usfm_resource_types + } + ) + ulb_usfm_resource_types_lang1 = ( + list( + { + usfm_resource_type_ + for usfm_resource_type_ in usfm_resource_types_lang1 + if "ulb" in usfm_resource_type_ + } + ) + if usfm_resource_types_lang1 + else [] + ) + usfm_books_lang1 = [] + usfm_resource_type_lang1 = "" + if ulb_usfm_resource_types_lang1: # Prefer ulb if available + usfm_resource_type_lang1 = ulb_usfm_resource_types_lang1[0] + elif usfm_resource_types_lang1: + usfm_resource_type_lang1 = usfm_resource_types_lang1[0] + if lang1_code and usfm_resource_type_lang1: + usfm_book2 = None + for book_code in book_codes_lang1: + current_task.update_state(state="Locating assets") + resource_lookup_dto_lang1 = resource_lookup_dto( + lang1_code, usfm_resource_type_lang1, book_code + ) + if resource_lookup_dto_lang1 and resource_lookup_dto_lang1.url: + current_task.update_state(state="Provisioning asset files") + resource_dir = prepare_resource_filepath(resource_lookup_dto_lang1) + provision_asset_files(resource_lookup_dto_lang1.url, resource_dir) + current_task.update_state(state="Parsing asset files") + usfm_book2 = usfm_book_content( + resource_lookup_dto_lang1, + resource_dir, + False, + ) + for chapter_num_, chapter_ in usfm_book2.chapters.items(): + usfm_book2.chapters[chapter_num_].verses = ( + split_chapter_into_verses(chapter_) + ) + usfm_books_lang1.append(usfm_book2) + current_task.update_state(state="Assembling content") + passages_lang0 = get_passages( + bible_references_with_availability_lang0, + usfm_resource_type_lang0, + usfm_books_lang0, + ) + passages_lang1 = get_passages( + bible_references_with_availability_lang1, + usfm_resource_type_lang1, + usfm_books_lang1, + ) current_task.update_state(state="Converting to Docx") - generate_docx(passages, docx_filepath_, lang_code, lang_name) + generate_docx( + passages_lang0, + passages_lang1, + docx_filepath_, + lang0_code, + lang0_name, + lang1_code, + lang1_name, + ) return docx_filepath_ def generate_docx( - passage_dtos: list[Passage], + passages_lang0: list[Passage], + passages_lang1: list[Passage], docx_filepath: str, - lang_code: str, - lang_name: str, - show_notes_column: bool = False, # TODO make a UI option, for now default to False + lang0_code: str, + lang0_name: str, + lang1_code: Optional[str], + lang1_name: Optional[str], + show_notes_column: bool = False, ) -> None: - """Generate the Passages output document in docx format""" + # logger.debug("passage_dtos: %s", passage_dtos) TOTAL_WIDTH = Inches(6.0) doc = Document() html_to_docx = HtmlToDocx() - for passage_dto in passage_dtos: - if show_notes_column: - table = doc.add_table(rows=1, cols=2) - left_col_width = Inches(4.0) - right_col_width = Inches(2.0) - col_widths = [left_col_width, right_col_width] - else: - table = doc.add_table(rows=1, cols=1) - col_widths = [TOTAL_WIDTH] - table.autofit = False - table.allow_autofit = False - # Apply column + cell widths explicitly - for i, width in enumerate(col_widths): - table.columns[i].width = width - cell = table.cell(0, i) - cell.width = width - tc = cell._tc - tcPr = tc.get_or_add_tcPr() - tcW = OxmlElement("w:tcW") - tcW.set(f"{{{WORD_NAMESPACE}}}w", str(int(width.inches * 1440))) - tcW.set(f"{{{WORD_NAMESPACE}}}type", "dxa") - tcPr.append(tcW) - # Fill left (or only) cell - cell_left = table.cell(0, 0) - # Add bible reference with color depending on availability - p = cell_left.add_paragraph() # create a fresh paragraph for control - run = p.add_run(passage_dto.bible_reference) - # run.bold = True # optional – if you want it emphasized - # run.font.size = Pt(10) # optional - if passage_dto.is_available: - run.font.color.rgb = RGBColor(102, 118, 139) # ≈ #66768B - else: - run.font.color.rgb = RGBColor(176, 184, 195) # ≈ #B0B8C3 - # Then add the passage text (still using html_to_docx if it has other formatting) - html_to_docx.add_html_to_document( - passage_dto.passage_text, - cell_left, + has_lang1 = lang1_code is not None and lang1_name is not None + if has_lang1: + assert len(passages_lang0) == len(passages_lang1), ( + f"Passage count mismatch: " + f"{len(passages_lang0)} vs {len(passages_lang1)}" + ) + columns: list[str] = ["lang0"] + if has_lang1: + columns.append("lang1") + if show_notes_column: + columns.append("notes") + if columns == ["lang0"]: + col_widths = [TOTAL_WIDTH] + elif columns == ["lang0", "lang1"]: + col_widths = [Inches(3.0), Inches(3.0)] + elif columns == ["lang0", "notes"]: + col_widths = [Inches(4.0), Inches(2.0)] + elif columns == ["lang0", "lang1", "notes"]: + col_widths = [Inches(2.5), Inches(2.5), Inches(1.0)] + else: + logger.warning(f"Unexpected column configuration: {columns}") + table = doc.add_table(rows=0, cols=len(columns)) + table.autofit = False + table.allow_autofit = False + for i, w in enumerate(col_widths): + table.columns[i].width = w + col_index = {name: i for i, name in enumerate(columns)} + logger.debug( + "len(lang0_passages): %s, len(lang1_passages): %s", + len(passages_lang0), + len(passages_lang1), + ) + pairs = ( + zip(passages_lang0, passages_lang1) + if has_lang1 + else ((p, None) for p in passages_lang0) + ) + for p0, p1 in pairs: + logger.debug("p0: %s, p1: %s", p0, p1) + row = table.add_row() + cell = row.cells[col_index["lang0"]] + run = cell.add_paragraph().add_run(p0.localized_reference) + run.font.color.rgb = ( + RGBColor(102, 118, 139) if p0.is_available else RGBColor(176, 184, 195) ) - # Fill right notes column only if enabled + if p0.is_available: + html_to_docx.add_html_to_document(p0.passage_text, cell) + if has_lang1 and p1 is not None: + cell = row.cells[col_index["lang1"]] + run = cell.add_paragraph().add_run(p1.localized_reference) + run.font.color.rgb = ( + RGBColor(102, 118, 139) if p1.is_available else RGBColor(176, 184, 195) + ) + if p1.is_available: + html_to_docx.add_html_to_document(p1.passage_text, cell) if show_notes_column: - cell_right = table.cell(0, 1) - cell_right.text = "" - add_vertical_line(cell_right) + cell = row.cells[col_index["notes"]] + cell.text = "" + add_vertical_line(cell) doc = add_footer(doc) - doc = add_header(doc, lang_name, header_text="Passages") + doc = add_header(doc, lang0_name, lang1_name, header_text="Passages") doc.save(docx_filepath) @@ -242,8 +373,9 @@ def add_vertical_line(cell: Cell) -> None: def document_request_key( - lang_code: str, - passage_reference_dtos: list[PassageReference], + lang0_code: str, + lang1_code: Optional[str], + passage_reference_dtos: list[BibleReferenceWithAvailability], max_filename_len: int = 240, underscore: str = "_", hyphen: str = "-", @@ -271,7 +403,11 @@ def document_request_key( for passage_reference in passage_reference_dtos ] ) - document_request_key_ = f"{lang_code}_{passages_key}_passages" + document_request_key_ = ( + f"{lang0_code}_{lang1_code}_{passages_key}_passages" + if lang1_code + else f"{lang0_code}_{passages_key}_passages" + ) 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 @@ -286,8 +422,10 @@ def document_request_key( @worker.app.task def generate_passages_docx_document( - lang_code: str, - lang_name: str, + lang0_code: str, + lang0_name: str, + lang1_code: Optional[str], + lang1_name: Optional[str], passage_reference_dtos_json: str, email_address: str, docx_filepath_prefix: str = "passages_", @@ -295,21 +433,26 @@ def generate_passages_docx_document( ) -> Json[str]: passage_reference_dtos_list = json.loads(passage_reference_dtos_json) passage_reference_dtos = [ - PassageReference(**d) for d in passage_reference_dtos_list + BibleReferenceWithAvailability(**d) for d in passage_reference_dtos_list ] - logger.debug( - "passed args: lang_code: %s, passage_references: %s, email_adress: %s", - lang_code, - passage_reference_dtos, - email_address, + # logger.debug( + # "passed args: lang0_code: %s, lang1_code: %s, passage_references: %s, email_adress: %s", + # lang0_code, + # lang1_code, + # passage_reference_dtos, + # email_address, + # ) + document_request_key_ = document_request_key( + lang0_code, lang1_code, passage_reference_dtos ) - document_request_key_ = document_request_key(lang_code, passage_reference_dtos) docx_filepath_ = f"{docx_filepath(document_request_key_, docx_filepath_prefix)}" logger.debug("docx_filepath_: %s", docx_filepath_) if file_needs_update(docx_filepath_): generate_docx_document( - lang_code, - lang_name, + lang0_code, + lang0_name, + lang1_code, + lang1_name, passage_reference_dtos, document_request_key_, docx_filepath_, @@ -339,6 +482,7 @@ def generate_passages_docx_document( def stet_exhaustive_verse_list( lang_code: str = "en", filepath: str = "backend/passages/data/Spiritual_Terms_Evaluation_Exhaustive_Verse_List.txt", + book_index: dict[str, int] = BOOK_INDEX, ) -> Sequence[BibleReference]: """ >>> from passages.domain.document_generator import stet_exhaustive_verse_list @@ -358,7 +502,7 @@ def stet_exhaustive_verse_list( unique_bible_references = sorted( set(bible_references), key=lambda ref: ( - BOOK_INDEX[ref.book_code], + book_index[ref.book_code], ref.start_chapter, ref.start_chapter_verse_ref, ), diff --git a/backend/passages/domain/model.py b/backend/passages/domain/model.py index 07dbf99e..5fdca812 100644 --- a/backend/passages/domain/model.py +++ b/backend/passages/domain/model.py @@ -1,4 +1,4 @@ -from typing import Optional, NamedTuple, final +from typing import Optional, NamedTuple, final, TypeAlias from doc.domain.model import ChapterNum from pydantic import BaseModel, EmailStr @@ -23,14 +23,17 @@ class BibleReferenceWithAvailability(BaseModel): @final class Passage(NamedTuple): + reference: BibleReference passage_text: str # HTML of passage - bible_reference: str + localized_reference: str is_available: bool @final class PassagesDocumentRequest(BaseModel): - lang_code: str - lang_name: str + lang0_code: str + lang0_name: str + lang1_code: Optional[str] + lang1_name: Optional[str] bible_references: list[BibleReferenceWithAvailability] email_address: Optional[EmailStr] diff --git a/backend/passages/entrypoints/routes.py b/backend/passages/entrypoints/routes.py index 1cbb66c4..55e7cbde 100644 --- a/backend/passages/entrypoints/routes.py +++ b/backend/passages/entrypoints/routes.py @@ -20,16 +20,17 @@ async def generate_passages_docx_document( passages_document_request: model.PassagesDocumentRequest, ) -> JSONResponse: - # logger.debug( - # "passages_document_request.bible_references: %s", - # passages_document_request.bible_references, - # ) - # Top level exception handler + logger.debug( + "passages_document_request: %s", + passages_document_request, + ) try: task = document_generator.generate_passages_docx_document.apply_async( args=( - passages_document_request.lang_code, - passages_document_request.lang_name, + passages_document_request.lang0_code, + passages_document_request.lang0_name, + passages_document_request.lang1_code, + passages_document_request.lang1_name, # Serialize the list of objects to a JSON string json.dumps( passages_document_request.bible_references, diff --git a/backend/passages/utils/docx_utils.py b/backend/passages/utils/docx_utils.py index 22b97799..74467601 100644 --- a/backend/passages/utils/docx_utils.py +++ b/backend/passages/utils/docx_utils.py @@ -119,7 +119,8 @@ def add_checkbox_to_cell(cell: Cell) -> None: def add_header( doc: DocxDocument, - lang_name: str, + lang0_name: str, + lang1_name: Optional[str], header_text: str = "Passages", ) -> DocxDocument: """ @@ -131,9 +132,16 @@ def add_header( header_paragraph = header.add_paragraph() header_paragraph.style = doc.styles["Header"] header_paragraph.style.font.size = Pt(12) # Optional: Adjust font size - # Add the header text with grey color - run1 = header_paragraph.add_run(header_text + ": " + lang_name) - run1.font.color.rgb = RGBColor(169, 169, 169) # Grey color + if lang1_name: + # Add the header text with grey color + run1 = header_paragraph.add_run( + header_text + ": " + lang0_name + "/" + lang1_name + ) + run1.font.color.rgb = RGBColor(169, 169, 169) # Grey color + else: + # Add the header text with grey color + run1 = header_paragraph.add_run(header_text + ": " + lang0_name) + run1.font.color.rgb = RGBColor(169, 169, 169) # Grey color return doc diff --git a/frontend/src/lib/bible-books.ts b/frontend/src/lib/bible-books.ts index f0939d15..424f1f78 100644 --- a/frontend/src/lib/bible-books.ts +++ b/frontend/src/lib/bible-books.ts @@ -70,11 +70,3 @@ export const booksMap: { [key: string]: string } = { export type BookKey = keyof typeof booksMap export const bookCodes = Object.keys(booksMap) as BookKey[] - -export function bookRange(start: string, end: string): BookKey[] { - const codes = bookCodes - const s = codes.indexOf(start) - const e = codes.indexOf(end) - if (s === -1 || e === -1 || s > e) return [] - return codes.slice(s, e + 1) -} diff --git a/frontend/src/lib/passages/DesktopBreadcrumb.svelte b/frontend/src/lib/passages/DesktopBreadcrumb.svelte index 478dbf52..3f2455a0 100644 --- a/frontend/src/lib/passages/DesktopBreadcrumb.svelte +++ b/frontend/src/lib/passages/DesktopBreadcrumb.svelte @@ -3,7 +3,7 @@ import { page, navigating } from '$app/stores' import BackButton from '$lib/BackButton.svelte' import NextButton from '$lib/NextButton.svelte' - import { langCodeAndNameStore } from '$lib/passages/stores/LanguagesStore' + import { langCodesStore } from '$lib/passages/stores/LanguagesStore' import { passagesStore } from '$lib/passages/stores/PassagesStore' import { getCode, langRegExp, passagesRegExp, settingsRegExp } from '$lib/passages/utils' import LeftArrowIcon from '$lib/LeftArrowIcon.svelte' @@ -100,7 +100,7 @@ {/if}
- {#if langRegExp.test($page.url.pathname) && $langCodeAndNameStore} + {#if langRegExp.test($page.url.pathname) && $langCodesStore} {:else if passagesRegExp.test($page.url.pathname) && $passagesStore} diff --git a/frontend/src/lib/passages/LanguageBasket.svelte b/frontend/src/lib/passages/LanguageBasket.svelte index c47a42a0..1c6d8456 100644 --- a/frontend/src/lib/passages/LanguageBasket.svelte +++ b/frontend/src/lib/passages/LanguageBasket.svelte @@ -2,38 +2,23 @@ import { goto } from '$app/navigation' import { page } from '$app/stores' import { - langCodeAndNameStore, - gatewayCodeAndNamesStore, - heartCodeAndNamesStore + langCodesStore, + langCountStore, + languagesClickedOrderStore } from '$lib/passages/stores/LanguagesStore' import { langRegExp, getCode, getName } from '$lib/passages/utils' import CloseIcon from '$lib/CloseIcon.svelte' import EditIcon from '$lib/EditIcon.svelte' import GlobeIcon from '$lib/GlobeIcon.svelte' - function uncheckGatewayLanguage(langCodeAndName: string) { - if (langCodeAndName) { - $gatewayCodeAndNamesStore = $gatewayCodeAndNamesStore.filter( - (item) => getCode(item) != getCode(langCodeAndName) - ) - } - if (langCodeAndName && $langCodeAndNameStore && langCodeAndName === $langCodeAndNameStore) { - $langCodeAndNameStore = '' - } + function uncheckLanguage(langCodeAndName: string) { + $languagesClickedOrderStore = $languagesClickedOrderStore.filter( + (item) => item != langCodeAndName + ) + $langCodesStore = $langCodesStore.filter((item) => item != getCode(langCodeAndName)) + $langCountStore = $langCodesStore.length } - function uncheckHeartLanguage(langCodeAndName: string) { - if (langCodeAndName) { - $heartCodeAndNamesStore = $heartCodeAndNamesStore.filter( - (item) => getCode(item) != getCode(langCodeAndName) - ) - } - if (langCodeAndName && $langCodeAndNameStore && langCodeAndName === $langCodeAndNameStore) { - $langCodeAndNameStore = '' - } - } - - $: console.log(`$langCodeAndNameStore: ${JSON.stringify(langCodeAndNameStore, null, 2)}`) {#if langRegExp.test($page.url.pathname)} @@ -56,40 +41,38 @@
{/if} -{#if $langCodeAndNameStore} - {#if langRegExp.test($page.url.pathname)} -
-
- {getName($langCodeAndNameStore)}({getCode($langCodeAndNameStore)}) -
- -
- {:else} -
({getCode(langCodeAndName)}) +
+ + + {:else} +
-
- {getName($langCodeAndNameStore)}({getCode($langCodeAndNameStore)}) + > +
+ {getName(langCodeAndName)}({getCode(langCodeAndName)}) +
-
- {/if} + {/if} + {/each} {:else}
- Language will appear here selected + Selections will appear here once a language is selected
{/if} diff --git a/frontend/src/lib/passages/MobileBreadcrumb.svelte b/frontend/src/lib/passages/MobileBreadcrumb.svelte index 2f6f4a8b..8b5dd89c 100644 --- a/frontend/src/lib/passages/MobileBreadcrumb.svelte +++ b/frontend/src/lib/passages/MobileBreadcrumb.svelte @@ -1,9 +1,8 @@ {#if passagesRegExp.test($page.url.pathname)} @@ -55,8 +54,8 @@ {/if} @@ -132,8 +144,8 @@
{#if passage.endChapter && passage.endChapter > 0 && passage.endChapterVerseRef} @@ -149,8 +161,8 @@ > {/if}
- {#if isAvailable(passage)} - {/if} diff --git a/frontend/src/lib/passages/utils.ts b/frontend/src/lib/passages/utils.ts index bdf9c3a2..22c13b6d 100644 --- a/frontend/src/lib/passages/utils.ts +++ b/frontend/src/lib/passages/utils.ts @@ -48,13 +48,17 @@ export function routeToPage(url: string): void { } } -export function isAvailable(passage: BibleReference): boolean { - const available = get(availablePassagesStore) +export function isAvailable( + passage: BibleReference, + available: readonly BibleReference[] +): boolean { return available.some( - (ref: BibleReference) => + (ref) => ref.langCode === passage.langCode && ref.bookCode === passage.bookCode && ref.startChapter === passage.startChapter && - ref.startChapterVerseRef === passage.startChapterVerseRef + ref.startChapterVerseRef === passage.startChapterVerseRef && + ref.endChapter === passage.endChapter && + ref.endChapterVerseRef === passage.endChapterVerseRef ) } diff --git a/frontend/src/routes/passages/settings/GenerateDocument.svelte b/frontend/src/routes/passages/settings/GenerateDocument.svelte index 581e8b15..8438e81b 100644 --- a/frontend/src/routes/passages/settings/GenerateDocument.svelte +++ b/frontend/src/routes/passages/settings/GenerateDocument.svelte @@ -7,7 +7,7 @@ langCountStore, langNamesStore } from '$lib/passages/stores/LanguagesStore' - import { passagesStore } from '$lib/passages/stores/PassagesStore' + import { passagesStore, availablePassagesStore } from '$lib/passages/stores/PassagesStore' import { emailStore, documentRequestKeyStore, @@ -17,7 +17,7 @@ import { isAvailable } from '$lib/passages/utils' import LogRocket from 'logrocket' import TaskStatus from './TaskStatus.svelte' - import type { PassagesDocumentRequest } from '$lib/passages/models/passage' + import type { PassagesDocumentRequest } from '$lib/passages/models' import { toSnakeCase } from '$lib/camel-to-snake-case-util' import ErrorAlertIcon from '$lib/ErrorAlertIcon.svelte' import { bookCodes } from '$lib/bible-books' @@ -77,7 +77,7 @@ (ref) => ({ reference: ref, - isAvailable: isAvailable(ref) + isAvailable: isAvailable(ref, availablePassages) }) as BibleReferenceWithAvailability ), emailAddress: $emailStore @@ -166,6 +166,8 @@ } }) + $: availablePassages = $availablePassagesStore + $: console.log('langNamesStore:', $langNamesStore) From a75fbd2084862d6de7846f94feb09f01384cc068 Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 3 Feb 2026 12:51:33 -0800 Subject: [PATCH 24/44] Adjustments for updates in prior commits And cleanup/simplification --- .../passages/AddPassageComponent.svelte | 59 ++++++++----------- .../passages/passages/AddSTETComponent.svelte | 23 ++------ 2 files changed, 27 insertions(+), 55 deletions(-) diff --git a/frontend/src/routes/passages/passages/AddPassageComponent.svelte b/frontend/src/routes/passages/passages/AddPassageComponent.svelte index d1a3af42..216f08df 100644 --- a/frontend/src/routes/passages/passages/AddPassageComponent.svelte +++ b/frontend/src/routes/passages/passages/AddPassageComponent.svelte @@ -1,6 +1,7 @@ diff --git a/frontend/src/routes/passages/language/+page.svelte b/frontend/src/routes/passages/language/+page.svelte index 408c8231..8a408cb0 100644 --- a/frontend/src/routes/passages/language/+page.svelte +++ b/frontend/src/routes/passages/language/+page.svelte @@ -6,6 +6,7 @@ import WizardBreadcrumb from '$lib/passages/WizardBreadcrumb.svelte' import WizardBasket from '$lib/passages/WizardBasket.svelte' import { passagesStore, availablePassagesStore } from '$lib/passages/stores/PassagesStore' + import type { BibleReference } from '$lib/passages/models' import { langCodesStore, @@ -25,8 +26,14 @@ return arr.filter((v) => v !== lang) } }) - $passagesStore = [] - $availablePassagesStore = [] + // keep only passages whose langCode is still selected + const allowedLangs = new Set($languagesClickedOrderStore) + passagesStore.update((passages) => + passages.filter((p: BibleReference) => p.langCode !== null && allowedLangs.has(p.langCode)) + ) + availablePassagesStore.update((passages) => + passages.filter((p: BibleReference) => p.langCode !== null && allowedLangs.has(p.langCode)) + ) } From 83054ef41d1b5b35085ae96370364f91116ba7de Mon Sep 17 00:00:00 2001 From: linearcombination <4829djaskdfj@gmail.com> Date: Tue, 3 Feb 2026 12:53:09 -0800 Subject: [PATCH 26/44] Update state of checkbox according to passages already chosen This makes checkboxes behave as expected when selected and then hitting next and then going back to select passages page or when selecting back and then going back to select passages page. Cleanup of logic --- .../passages/passages/AddNTComponent.svelte | 111 ++++++------------ 1 file changed, 39 insertions(+), 72 deletions(-) diff --git a/frontend/src/routes/passages/passages/AddNTComponent.svelte b/frontend/src/routes/passages/passages/AddNTComponent.svelte index fe36827e..38e68cf2 100644 --- a/frontend/src/routes/passages/passages/AddNTComponent.svelte +++ b/frontend/src/routes/passages/passages/AddNTComponent.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte' import ProgressIndicator from '$lib/ProgressIndicator.svelte' import { + passagesStore, addBibleReference, addAvailableBibleReference, removeBibleReference @@ -9,7 +10,8 @@ import { langCodesStore, langCountStore } from '$lib/passages/stores/LanguagesStore' import { PUBLIC_NT_SURVEY_RG_PASSAGES_URL } from '$env/static/public' import { env } from '$env/dynamic/public' - import type { BibleReference } from './model' + import type { BibleReference } from '$lib/passages/models' + import { matches, parseBibleReferences } from '$lib/passages/models' let loading: boolean = false export let checkIcon: string @@ -22,6 +24,7 @@ let availableLang0NtBibleReferences: Array = [] let availableLang1NtBibleReferences: Array = [] let showNT: boolean = false + let checkboxChecked: boolean async function handleAddNTSurveyRGPassagesClick() { isLoadingNTSurvey = true @@ -64,12 +67,12 @@ const url = `${apiRootUrl}${ntSurveyRgPassagesUrl}/${langCode}` console.log(`url: ${url}`) const response = await fetch(url) - const bibleReferences: Array = await response.json() + const json = await response.json() if (!response.ok) { console.error(response.statusText) throw new Error(response.statusText) } - return bibleReferences + return parseBibleReferences.parse(json) } onMount(async () => { @@ -78,53 +81,27 @@ lang0NtBibleReferences = await getNTSurveyRGPassages($langCodesStore[0]) // Filter down to the passages available in this language availableLang0NtBibleReferences = lang0NtBibleReferences.filter((ref) => - bookCodesAndNamesLang0.some(([code]) => code === ref.book_code) + bookCodesAndNamesLang0.some(([code]) => code === ref.bookCode) ) if ($langCountStore > 1) { lang1NtBibleReferences = await getNTSurveyRGPassages($langCodesStore[1]) // Filter down to the passages available in this language availableLang1NtBibleReferences = lang1NtBibleReferences.filter((ref) => - bookCodesAndNamesLang1.some(([code]) => code === ref.book_code) + bookCodesAndNamesLang1.some(([code]) => code === ref.bookCode) ) } - // Add availableLang0NtBibleReferences to filteredPassagesStore for - // later reference in PassagesBasket for (const bibleRef of availableLang0NtBibleReferences) { - addAvailableBibleReference( - $langCodesStore[0], - bibleRef.book_code, - bibleRef.book_name, - Number(bibleRef.start_chapter), - bibleRef.start_chapter_verse_ref, - Number(bibleRef.end_chapter), - bibleRef.end_chapter_verse_ref - ) + addAvailableBibleReference(bibleRef) } - // Add availableLang1NtBibleReferences to filteredPassagesStore for - // later reference in PassagesBasket for (const bibleRef of availableLang1NtBibleReferences) { - addAvailableBibleReference( - $langCodesStore[1], - bibleRef.book_code, - bibleRef.book_name, - Number(bibleRef.start_chapter), - bibleRef.start_chapter_verse_ref, - Number(bibleRef.end_chapter), - bibleRef.end_chapter_verse_ref - ) + addAvailableBibleReference(bibleRef) } - // Set flag indicating if this language provides any of the NT - // RG survey passages. We use this to show or not show the NT RG - // passages checkbox - showNT = - availableLang0NtBibleReferences.length > 0 || availableLang1NtBibleReferences.length > 0 - loading = false } catch (error) { console.error('Failed to load NT Survey RG passages:', error) } finally { console.log('NT Survey RG passages loaded successfully') + loading = false } - loading = false }) export async function addNTSurveyRGPassages() { @@ -132,26 +109,10 @@ // Add lang0 NT RG passages to the passageStore for reference in // PassagesBasket.svelte for (const bibleRef of lang0NtBibleReferences) { - addBibleReference( - $langCodesStore[0], - bibleRef.book_code, - bibleRef.book_name, - Number(bibleRef.start_chapter), - bibleRef.start_chapter_verse_ref, - Number(bibleRef.end_chapter), - bibleRef.end_chapter_verse_ref - ) + addBibleReference(bibleRef) } for (const bibleRef of lang1NtBibleReferences) { - addBibleReference( - $langCodesStore[1], - bibleRef.book_code, - bibleRef.book_name, - Number(bibleRef.start_chapter), - bibleRef.start_chapter_verse_ref, - Number(bibleRef.end_chapter), - bibleRef.end_chapter_verse_ref - ) + addBibleReference(bibleRef) } } catch (error) { console.error('Failed to add NT Survey RG passages:', error) @@ -164,25 +125,11 @@ try { // Remove the lang0 NT RG passages from the passageStore for (const bibleRef of lang0NtBibleReferences) { - removeBibleReference( - $langCodesStore[0], - bibleRef.book_code, - Number(bibleRef.start_chapter), - bibleRef.start_chapter_verse_ref, - Number(bibleRef.end_chapter), - bibleRef.end_chapter_verse_ref - ) + removeBibleReference(bibleRef) } // Remove the lang1 NT RG passages from the passageStore for (const bibleRef of lang1NtBibleReferences) { - removeBibleReference( - $langCodesStore[1], - bibleRef.book_code, - Number(bibleRef.start_chapter), - bibleRef.start_chapter_verse_ref, - Number(bibleRef.end_chapter), - bibleRef.end_chapter_verse_ref - ) + removeBibleReference(bibleRef) } } catch (error) { console.error('Failed to remove NT Survey RG passages:', error) @@ -190,10 +137,29 @@ console.log('NT Survey RG Passages removed successfully') } } - $: console.log('lang0NtBibleReferences:', lang0NtBibleReferences) - $: console.log('lang1NtBibleReferences:', lang1NtBibleReferences) - $: console.log('availableLang0NtBibleReferences:', availableLang0NtBibleReferences) - $: console.log('availableLang1NtBibleReferences:', availableLang1NtBibleReferences) + $: { + console.log('lang0NtBibleReferences:', lang0NtBibleReferences) + console.log('lang1NtBibleReferences:', lang1NtBibleReferences) + console.log('availableLang0NtBibleReferences:', availableLang0NtBibleReferences) + console.log('availableLang1NtBibleReferences:', availableLang1NtBibleReferences) + } + + + $: checkboxChecked = + lang0NtBibleReferences.every((ref) => + $passagesStore.some((storeRef) => matches(storeRef, ref)) + ) && + ($langCountStore > 1 + ? lang1NtBibleReferences.every((ref) => + $passagesStore.some((storeRef) => matches(storeRef, ref)) + ) + : true) + + // Set flag indicating if this language provides any of the NT + // RG survey passages. We use this to show or not show the NT RG + // passages checkbox + $: showNT = + availableLang0NtBibleReferences.length > 0 || availableLang1NtBibleReferences.length > 0 {#if loading} @@ -205,6 +171,7 @@ id="add-nt-survey-passages-checkbox" type="checkbox" class="checkbox-target checkbox-style" + checked={checkboxChecked} on:click={handleNTSurveyCheckboxClick} />