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}