diff --git a/Makefile b/Makefile index bc2fc143..bde08c6a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,21 @@ +.PHONY: startdocker +startdocker: + @if docker info >/dev/null 2>&1; then \ + echo "Docker is already running."; \ + else \ + echo "Starting Docker..."; \ + if [ "$$(uname)" = "Darwin" ]; then \ + open -a Docker; \ + else \ + sudo systemctl start docker; \ + fi; \ + echo "Waiting for Docker to fully initialize..."; \ + until docker info >/dev/null 2>&1; do sleep 1; done; \ + echo "Docker is ready!"; \ + fi + .PHONY: checkvenv -checkvenv: +checkvenv: startdocker # raises error if environment is not active ifeq ("$(VIRTUAL_ENV)","") @echo "Venv is not activated!" @@ -133,7 +149,7 @@ e2e-docx-tests: clean-local-docker-output-dir .PHONY: frontend-tests -frontend-tests: +frontend-tests: startdocker # NOTE If we are experiencing some issues with the docker # compose running of frontend tests, we can still use the # non-Dockerized approach successfully. Doing so requires that @@ -184,7 +200,7 @@ clean-mypyc-artifacts: find . ! -path .venv -type f -name "*.c" -exec rm -- {} + .PHONY: prune-docker-images-volumes -prune-docker-images-volumes: +prune-docker-images-volumes: checkvenv docker system prune --volumes # https://radon.readthedocs.io/en/latest/commandline.html @@ -301,7 +317,8 @@ local-run-celery: .PHONY: local-run-flower local-run-flower: - celery --broker=redis:// --result-backend=redis:// flower + celery --broker=redis:// --result-backend=redis:// flower + # This is one to run after running local-e2e-tests or any tests which # has yielded HTML and PDFs that need to be checked for linking # correctness. diff --git a/backend/doc/config.py b/backend/doc/config.py index f2bf1cfc..a5c63522 100755 --- a/backend/doc/config.py +++ b/backend/doc/config.py @@ -20,6 +20,8 @@ class Settings(BaseSettings): DATA_API_URL: HttpUrl + LANGUAGES_WHERE_NON_ULB_PREFERRED: Sequence[str] = ["fr"] + # This is only used to see if a lang_code is in the collection # otherwise it is a heart language. Eventually the graphql data api may # provide gateway/heart boolean value. diff --git a/backend/doc/domain/assembly_strategies/assemble_by_book.py b/backend/doc/domain/assembly_strategies/assemble_by_book.py index f0c03c50..824670ce 100755 --- a/backend/doc/domain/assembly_strategies/assemble_by_book.py +++ b/backend/doc/domain/assembly_strategies/assemble_by_book.py @@ -24,7 +24,7 @@ TWBook, USFMBook, ) -from doc.domain.parsing import handle_split_chapter_into_verses +from doc.domain.parsing import split_chapter_into_verses_with_formatting from doc.reviewers_guide.model import RGBook logger = settings.logger(__name__) @@ -475,7 +475,7 @@ def assemble_usfm_by_verse_book_at_a_time( chapter_num, chapter, ) in usfm_book.chapters.items(): - chapter.verses = handle_split_chapter_into_verses(usfm_book, chapter) + chapter.verses = split_chapter_into_verses_with_formatting(chapter) chapter_intros = get_chapter_intros( tn_book, tnc_book, @@ -531,8 +531,10 @@ def assemble_usfm_by_verse_book_at_a_time( # ulb, f10, show the second USFM content here if usfm_book2: usfm_book2_chapter = usfm_book2.chapters[chapter_num] - usfm_book2_chapter.verses = handle_split_chapter_into_verses( - usfm_book2, usfm_book2_chapter + usfm_book2_chapter.verses = ( + split_chapter_into_verses_with_formatting( + usfm_book2_chapter + ) ) if ( usfm_book2_chapter.verses diff --git a/backend/doc/domain/assembly_strategies/assemble_by_chapter.py b/backend/doc/domain/assembly_strategies/assemble_by_chapter.py index e141fb4d..29ff87d1 100644 --- a/backend/doc/domain/assembly_strategies/assemble_by_chapter.py +++ b/backend/doc/domain/assembly_strategies/assemble_by_chapter.py @@ -37,10 +37,9 @@ TWBook, USFMBook, ) -from doc.domain.parsing import handle_split_chapter_into_verses +from doc.domain.parsing import split_chapter_into_verses_with_formatting from doc.reviewers_guide.model import RGBook - logger = settings.logger(__name__) @@ -625,13 +624,13 @@ def assemble_usfm_by_verse_chapter_at_a_time( primary_verses = None secondary_verses = None if usfm_book and usfm_chapter: - usfm_chapter.verses = handle_split_chapter_into_verses( - usfm_book, usfm_chapter + usfm_chapter.verses = split_chapter_into_verses_with_formatting( + usfm_chapter ) primary_verses = usfm_chapter.verses if usfm_book2 and usfm_book2_chapter: - usfm_book2_chapter.verses = handle_split_chapter_into_verses( - usfm_book2, usfm_book2_chapter + usfm_book2_chapter.verses = split_chapter_into_verses_with_formatting( + usfm_book2_chapter ) secondary_verses = usfm_book2_chapter.verses if usfm_book and primary_verses: diff --git a/backend/doc/domain/document_generator.py b/backend/doc/domain/document_generator.py index 7d223e58..8032863f 100755 --- a/backend/doc/domain/document_generator.py +++ b/backend/doc/domain/document_generator.py @@ -51,6 +51,7 @@ from doc.utils.docx_util import ( add_internal_docx_links, generate_docx_toc, + override_and_clean_hyperlinks, style_superscripts, ) from doc.utils.file_utils import ( @@ -778,6 +779,15 @@ def compose_docx_document( if part.add_page_break: add_page_break(doc) style_superscripts(doc, lift_half_points=2, color=None) + # html4doc defeats normal use of hyperlink inline styling in Word + # via a customized (otherwise standard) Hyperlink style, but + # this handles it by doing a pass over the document after the fact + # and forcing hyperlinks to render using a specific style we + # created to match the PO's desired look. + # PlainHyperlinkChar is a character style we created in + # template.docx, but its actual style ID is PlainHyperlinkChar0 + # under the hood. + override_and_clean_hyperlinks(doc, "PlainHyperlinkChar0") t1 = time.time() logger.info("Time for converting HTML to Docx: %.2f seconds", t1 - t0) return doc diff --git a/backend/doc/domain/parsing.py b/backend/doc/domain/parsing.py index 2423f68c..03a2e3e4 100644 --- a/backend/doc/domain/parsing.py +++ b/backend/doc/domain/parsing.py @@ -2,6 +2,7 @@ This module provides an API for parsing content. """ +from bs4 import BeautifulSoup, NavigableString from re import ( compile, escape, @@ -276,14 +277,19 @@ def split_usfm_by_chapters( chapters = re_split(chapter_regex, usfm_text) frontmatter = chapters.pop(0).strip() + defective_lang_codes = {resource[0] for resource in resources_with_usfm_defects} + def needs_fixing() -> bool: """ - Determine if a chapter needs fixing based on configuration. + Determine whether this resource should be checked for known USFM defects. + + If CHECK_ALL_BOOKS_FOR_LANGUAGE is enabled, then the presence of any + known defective resource for a language causes all books for that + language to be checked for similar defects. Otherwise, only explicitly + listed resource tuples are checked. """ if check_all_books_for_language: - return lang_code in [ - resource[0] for resource in resources_with_usfm_defects - ] + return lang_code in defective_lang_codes return ( lang_code, resource_type, @@ -527,9 +533,7 @@ def usfm_book_content( cleaned_chapter_html_content_ = remove_null_bytes_and_control_characters( chapter_html_content ) - cleaned_chapter_html_content = remove_unwanted_elements( - cleaned_chapter_html_content_ - ) + cleaned_chapter_html_content = clean_content_html(cleaned_chapter_html_content_) usfm_chapters[chapter_num] = USFMChapter( content=( cleaned_chapter_html_content if cleaned_chapter_html_content else "" @@ -544,7 +548,7 @@ def usfm_book_content( national_book_name=( localized_book_name if localized_book_name - else BOOK_NAMES[resource_lookup_dto.book_code] + else book_names[resource_lookup_dto.book_code] ), resource_type_name=resource_lookup_dto.resource_type_name, chapters=usfm_chapters if usfm_chapters else {}, @@ -1408,72 +1412,13 @@ def lookup_verse_text(usfm_book: USFMBook, chapter_num: int, verse_ref: str) -> return verse -# Used by STET and PASSAGES apps -def split_chapter_into_verses(chapter: USFMChapter) -> dict[str, str]: - # Sample HTML content with multiple verse elements - # html_content = ''' - # - # 19 - # For through the law I died to the law, so that I might live for God. I have been crucified with Christ. - # 1 - #
- #
- # - # 20 - # I have been crucified with Christ and I no longer live, but Christ lives in me. The life I now live in the body, I live by faith in the Son of God, who loved me and gave himself for me. - # 2 - #
- #
- # ''' - verse_dict = {} - # Find all verse spans - verse_spans = findall(r'(.*?)', chapter.content, DOTALL) - for verse_span in verse_spans: - # Extract the verse number from the versemarker - verse_number = search(r'(\d+)', verse_span) - if verse_number: - verse_number_ = verse_number.group(1) - # Remove versemarker - verse_text = sub(r'.*?', "", verse_span) - # Remove footnotes numbers - verse_text = sub(r'.*?', "", verse_text) - # Fix spacing issue when div class="poetry-*" type divs - # are used, e.g., yielding 'heartsas' for Hebrews 3:8 - verse_text = sub( - r'
(.*?)
', - r" \1", - verse_text, - ) - # Add to the dictionary with verse number as the key and verse text as the value - verse_dict[verse_number_] = verse_text - return verse_dict - - -def handle_split_chapter_into_verses( - usfm_book: USFMBook, - usfm_chapter: USFMChapter, - resource_type_codes_and_names: Mapping[ - str, str - ] = settings.RESOURCE_TYPE_CODES_AND_NAMES, -) -> dict[VerseRef, str]: - if ( - usfm_book.lang_code == "fr" - and usfm_book.resource_type_name == resource_type_codes_and_names["f10"] - ): - return split_chapter_into_verses_with_formatting_for_f10(usfm_chapter) - else: - return split_chapter_into_verses_with_formatting(usfm_chapter) - - def split_chapter_into_verses_with_formatting( chapter: USFMChapter, - empty_paragraph: str = "

", - sectionhead5_element: str = '
', ) -> dict[VerseRef, str]: """ - Given a USFMChapter instance, return the same instance with its - verses attribute set to a dictionary where the key is the verse - number and the value is the verse HTML. + Parse chapter.content as HTML, extract each , + unwrap elements (preserving their text), + and return a dict mapping verse number -> cleaned HTML fragment for that verse. Sample HTML content with multiple verse elements: @@ -1500,74 +1445,34 @@ def split_chapter_into_verses_with_formatting( 1 """ - # TODO What to do about footnote targets? Perhaps have the value be a - # tuple with first element of the verse HTML (which includes the - # footnote callers) and the second element the target footnotes HTML? - verse_dict = {} - # Find all verse spans - verse_spans = findall(r'(.*?)', chapter.content, DOTALL) - for verse_span in verse_spans: - # Extract the verse number from the versemarker - verse_number = search(r'(\d+)', verse_span) - if verse_number: - verse_number_ = verse_number.group(1) - # Add to the dictionary with verse number as the key and verse text as the value - verse_dict[verse_number_] = ( - verse_span.strip() - .replace(empty_paragraph, "") - .replace(sectionhead5_element, "") - ) - return verse_dict - - -def split_chapter_into_verses_with_formatting_for_f10( - chapter: USFMChapter, - empty_paragraph: str = "

", - sectionhead5_element: str = '
', -) -> dict[str, str]: - """ - Parse chapter.content as HTML, extract each , - unwrap elements (preserving their text), - and return a dict mapping verse number -> cleaned HTML fragment for that verse. - """ soup = BeautifulSoup(chapter.content, "html.parser") - verse_dict: dict[str, str] = {} - # find all verse spans (parser handles nesting correctly) + verse_dict: dict[VerseRef, str] = {} for verse_span in soup.find_all("span", class_="verse"): - # find the verse number from NN sup = verse_span.find("sup", class_="versemarker") if not sup or not sup.string: continue verse_number = sup.string.strip() - # unwrap all word-entry spans: replace X - # with X (preserving whitespace/punctuation) + # Remove the versemarker number sup + sup.decompose() + # fr f10 uses word-entry tags for we in verse_span.find_all("span", class_="word-entry"): we.unwrap() - # Option: normalize whitespace (optional) - # If you want to preserve original spacing/punctuation exactly, skip this. - # cleaned_html = "".join(str(c) for c in verse_span.contents) - cleaned_html = str(verse_span) - # Fix spacing issues introduced by inner spans - cleaned_html = sub( - r"\s+([,;:.!?])", r"\1", cleaned_html - ) # remove space before punctuation - cleaned_html = sub(r"\s+'", "'", cleaned_html) # remove space before apostrophe - cleaned_html = sub(r"'\s+", "'", cleaned_html) # remove space after apostrophe - cleaned_html = sub( - r"\s*-\s*", "-", cleaned_html - ) # normalize spaces around hyphens - cleaned_html = sub(r"\s{2,}", " ", cleaned_html) # collapse double spaces - cleaned_html = cleaned_html.strip() - # if you want plain text instead, use: cleaned_text = verse_span.get_text(" ", strip=True) - # store cleaned HTML fragment (still contains etc.) - verse_dict[verse_number] = ( - cleaned_html.strip() - .replace(empty_paragraph, "") - .replace(sectionhead5_element, "") - ) + cleaned_html = clean_content_html(str(verse_span)) + verse_dict[verse_number] = cleaned_html return verse_dict +def clean_content_html(raw_content: str) -> str: + soup = BeautifulSoup(raw_content, "html.parser") + cleaned_html = str(soup) + cleaned_html = sub(r"\s+([,;:.!?])", r"\1", cleaned_html) + cleaned_html = sub(r"\s+'", "'", cleaned_html) + cleaned_html = sub(r"'\s+", "'", cleaned_html) + cleaned_html = sub(r"\s*-\s*", "-", cleaned_html) + cleaned_html = sub(r"\s{2,}", " ", cleaned_html).strip() + return cleaned_html + + if __name__ == "__main__": # To run the doctests in this module, in the root of the project do: diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index f3da9d4c..ee7ade7d 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -55,15 +55,6 @@ ) -# List of languages which do not have USFM available for any books. We use this -# to filter these out of STET's list of source and target -# languages so that the user doesn't have the frustrating experience of -# selecting a language which might have non-USFM resources available but -# not USFM so that when their resulting doc is generated no scripture is -# present. It makes it seem like a bug in STET and is bad UX. -LANG_CODES_WITH_NO_USFM: frozenset[str] = frozenset(["ru"]) - - @cached(fetch_source_data_cache) def fetch_source_data( data_api_url: HttpUrl = settings.DATA_API_URL, @@ -175,46 +166,6 @@ def lang_codes_and_names( return sorted(unique_values, key=lambda value: value[1]) -def lang_codes_and_names_having_usfm( - lang_code_filter_list: frozenset[str] = LANG_CODES_WITH_NO_USFM, - gateway_languages: frozenset[str] = settings.GATEWAY_LANGUAGES, -) -> Sequence[tuple[str, str, bool]]: - """ - >>> from doc.domain import resource_lookup - >>> ();result = resource_lookup.lang_codes_and_names_having_usfm();() # doctest: +ELLIPSIS - (...) - >>> result[0] - ('abz', 'Abui', False) - >>> heart_lang_codes = [lang_code_and_name[0] for lang_code_and_name in resource_lookup.lang_codes_and_names_having_usfm() if not lang_code_and_name[2]] - >>> sorted(heart_lang_codes)[0] - 'aao' - """ - data = fetch_source_data() - values = [] - if data is None or not data.git_repo: - logger.info("Data API is down or no git_repo found!") - return [] - try: - for repo_info in data.git_repo: - language_info = repo_info.content - language = language_info.language - ietf_code = language.ietf_code - english_name = language.english_name if language.english_name else "" - localized_name = language.national_name - is_gateway = ietf_code in gateway_languages - if ietf_code not in lang_code_filter_list: - if english_name in localized_name: - values.append((ietf_code, localized_name, is_gateway)) - else: - values.append( - (ietf_code, f"{localized_name} ({english_name})", is_gateway) - ) - except Exception: - logger.exception("Failed due to the following exception.") - unique_values = unique_tuples(values) - return sorted(unique_values, key=lambda value: value[1]) - - def repos_to_clone( lang_code: str, augmented_repos_info: list[RepoEntry], @@ -289,22 +240,6 @@ def get_resource_types( ] elif resource_type in usfm_resource_types: book_assets = find_usfm_files(resource_filepath) - elif resource_type == "rg": - between_texts, bible_reference_strs = find_bible_references( - join(en_rg, docx_file_path) - ) - bible_references = [ - parse_bible_reference(bible_reference) - for bible_reference in bible_reference_strs - ] - book_codes_ = { - bible_reference.book_code - for bible_reference in bible_references - if bible_reference - } - book_assets = [ - book_code for book_code in book_codes if book_code in book_codes_ - ] if book_assets or resource_type == "tw": resource_types.append( ( diff --git a/backend/doc/domain/usfm_error_detection_and_fixes.py b/backend/doc/domain/usfm_error_detection_and_fixes.py index f97268b8..7d9a9575 100644 --- a/backend/doc/domain/usfm_error_detection_and_fixes.py +++ b/backend/doc/domain/usfm_error_detection_and_fixes.py @@ -21,7 +21,6 @@ ("adn", "reg", "mat"), ("agd-x-namel", "reg", "2th"), ("ahm", "reg", "php"), - ("ahm", "reg", "php"), ("ajg-x-adjtalagbe", "reg", "mat"), ("aoa", "reg", "col"), ("aoa", "reg", "luk"), @@ -52,7 +51,6 @@ ("blo", "reg", "php"), ("blo", "reg", "heb"), ("blo", "reg", "1jn"), - ("blo", "reg", "col"), ("bne", "reg", "gal"), ("bof", "reg", "mat"), ("bou", "reg", "gen"), diff --git a/backend/doc/utils/docx_util.py b/backend/doc/utils/docx_util.py index 2f35821d..f51797a4 100644 --- a/backend/doc/utils/docx_util.py +++ b/backend/doc/utils/docx_util.py @@ -56,6 +56,33 @@ def _make_text_run(text: str) -> Element: return r +def override_and_clean_hyperlinks(doc: DocxDocument, style_id: str) -> None: + """ + Finds all hyperlinks, strips any forced inline formatting (like blue color), + and applies the correct template character style ID using raw XML manipulation. + """ + for paragraph in doc.paragraphs: + hyperlinks = paragraph._p.findall( + ".//{http://schemas.openxmlformats.org/wordprocessingml/2006/main}hyperlink" + ) + for hyperlink in hyperlinks: + runs = hyperlink.findall(qn("w:r")) + for r in runs: + rPr = r.find(qn("w:rPr")) + if rPr is None: + rPr = OxmlElement("w:rPr") + r.insert(0, rPr) + existing_style = rPr.find(qn("w:rStyle")) + if existing_style is not None: + rPr.remove(existing_style) + for child in list(rPr): + if child.tag in (qn("w:color"), qn("w:u"), qn("w:shd")): + rPr.remove(child) + rStyle = OxmlElement("w:rStyle") + rStyle.set(qn("w:val"), style_id) + rPr.insert(0, rStyle) + + def _make_internal_hyperlink_element(text: str, bookmark_name: str) -> Element: hyperlink = OxmlElement("w:hyperlink") hyperlink.set(qn("w:anchor"), bookmark_name) diff --git a/backend/passages/domain/document_generator.py b/backend/passages/domain/document_generator.py index 4a79e56d..841d7334 100644 --- a/backend/passages/domain/document_generator.py +++ b/backend/passages/domain/document_generator.py @@ -8,7 +8,9 @@ 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, USFMBook -from doc.domain.parsing import split_chapter_into_verses, usfm_book_content +from doc.domain.parsing import ( + usfm_book_content, +) from doc.domain.resource_lookup import ( book_codes_for_lang_from_usfm_only, prepare_resource_filepath, @@ -29,7 +31,7 @@ Passage, BibleReferenceWithAvailability, ) -from passages.domain.parser import verse_text_html +from passages.domain.parser import split_chapter_into_verses, 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 @@ -101,6 +103,9 @@ def get_usfm_books_and_usfm_resource_type( bible_references_with_availability: list[BibleReferenceWithAvailability], lang_code: str, usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, + languages_where_non_ulb_preferred: Sequence[ + str + ] = settings.LANGUAGES_WHERE_NON_ULB_PREFERRED, ) -> tuple[list[USFMBook], str]: # Invariant: book codes are only those that were available from USFM resources book_codes = list( @@ -128,10 +133,16 @@ def get_usfm_books_and_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 lang_code not in languages_where_non_ulb_preferred: + 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] + else: + if usfm_resource_types: # Prefer non-ulb if available + usfm_resource_type = usfm_resource_types[0] + elif ulb_usfm_resource_types: + usfm_resource_type = ulb_usfm_resource_types[0] if usfm_resource_type: usfm_book = None for book_code in book_codes: @@ -197,22 +208,22 @@ def generate_docx_document( ) ) current_task.update_state(state="Assembling content") - passages_lang0 = get_passages( + lang0_passages = get_passages( bible_references_with_availability_lang0, usfm_resource_type_lang0, usfm_books_lang0, ) - passages_lang1 = [] + lang1_passages = [] if lang1_code: - passages_lang1 = get_passages( + lang1_passages = 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_lang0, - passages_lang1, + lang0_passages, + lang1_passages, docx_filepath_, lang0_code, lang0_name, @@ -223,8 +234,8 @@ def generate_docx_document( def generate_docx( - passages_lang0: list[Passage], - passages_lang1: list[Passage], + lang0_passages: list[Passage], + lang1_passages: list[Passage], docx_filepath: str, lang0_code: str, lang0_name: str, @@ -245,9 +256,9 @@ def generate_docx( html_to_docx = HtmlToDocx() has_lang1 = lang1_code is not None and lang1_name is not None if has_lang1: - assert len(passages_lang0) == len(passages_lang1), ( + assert len(lang0_passages) == len(lang1_passages), ( f"Passage count mismatch: " - f"{len(passages_lang0)} vs {len(passages_lang1)}" + f"{len(lang0_passages)} vs {len(lang1_passages)}" ) columns: list[str] = ["lang0"] if has_lang1: @@ -271,9 +282,9 @@ def generate_docx( table.columns[i].width = w col_index = {name: i for i, name in enumerate(columns)} pairs = ( - zip(passages_lang0, passages_lang1) + zip(lang0_passages, lang1_passages) if has_lang1 - else ((p, None) for p in passages_lang0) + else ((p, None) for p in lang0_passages) ) for p0, p1 in pairs: row = table.add_row() diff --git a/backend/passages/domain/parser.py b/backend/passages/domain/parser.py index 455f3366..210704d4 100644 --- a/backend/passages/domain/parser.py +++ b/backend/passages/domain/parser.py @@ -1,12 +1,13 @@ +from re import split, sub from typing import Mapping +from bs4 import BeautifulSoup, NavigableString from doc.config import settings from doc.domain.bible_books import BOOK_CHAPTER_VERSES -from doc.domain.model import USFMBook +from doc.domain.model import USFMBook, USFMChapter from doc.domain.parsing import lookup_verse_text from doc.reviewers_guide.model import BibleReference - logger = settings.logger(__name__) @@ -102,3 +103,63 @@ def verse_text_html( f'{bible_reference.start_chapter_verse_ref.strip()}{verse_text___}' ) return "".join(verse_text) + + +def split_chapter_into_verses(chapter: USFMChapter) -> dict[str, str]: + # Sample HTML content with multiple verse elements + # html_content = ''' + # + # 19 + # For through the law I died to the law, so that I might live for God. I have been crucified with Christ. + # 1 + #
+ #
+ # + # 20 + # I have been crucified with Christ and I no longer live, but Christ lives in me. The life I now live in the body, I live by faith in the Son of God, who loved me and gave himself for me. + # 2 + #
+ #
+ # ''' + verse_dict: dict[str, str] = {} + soup = BeautifulSoup(chapter.content, "html.parser") + for verse_span in soup.find_all("span", class_="verse"): + versemarker = verse_span.find("sup", class_="versemarker") + if not versemarker or not versemarker.get_text(strip=True): + continue + verse_number = versemarker.get_text(strip=True) + # Remove verse marker + versemarker.decompose() + # Remove footnote callers + for caller in verse_span.find_all("sup", class_="caller"): + caller.decompose() + # Fix spacing issue for poetry divs + for poetry_div in verse_span.find_all( + "div", class_=lambda c: c and c.startswith("poetry-") + ): + poetry_div.insert_before(NavigableString(" ")) + # Handle fr f10 word-entry tags + for we in verse_span.find_all("span", class_="word-entry"): + we.unwrap() + # Get inner HTML of the verse span + verse_text = "".join(str(child) for child in verse_span.contents).strip() + # verse_text = clean_verse_html(verse_text) + verse_dict[verse_number] = verse_text + return verse_dict + + +def clean_verse_html( + raw_verse: str, + empty_paragraph: str = "

", + sectionhead5_element: str = '
', +) -> str: + cleaned_html = raw_verse + cleaned_html = sub(r"\s+([,;:.!?])", r"\1", cleaned_html) + cleaned_html = sub(r"\s+'", "'", cleaned_html) + cleaned_html = sub(r"'\s+", "'", cleaned_html) + cleaned_html = sub(r"\s*-\s*", "-", cleaned_html) + cleaned_html = sub(r"\s{2,}", " ", cleaned_html).strip() + cleaned_html = cleaned_html.replace(empty_paragraph, "").replace( + sectionhead5_element, "" + ) + return cleaned_html diff --git a/backend/stet/data/stet_gu.docx b/backend/stet/data/stet_gu.docx new file mode 100644 index 00000000..7e3601cd Binary files /dev/null and b/backend/stet/data/stet_gu.docx differ diff --git a/backend/stet/data/stet_rmn-x-yerliroman.docx b/backend/stet/data/stet_rmn-x-yerliroman.docx new file mode 100644 index 00000000..2508f455 Binary files /dev/null and b/backend/stet/data/stet_rmn-x-yerliroman.docx differ diff --git a/backend/stet/data/stet_ru.docx b/backend/stet/data/stet_ru.docx new file mode 100644 index 00000000..d426b43d Binary files /dev/null and b/backend/stet/data/stet_ru.docx differ diff --git a/backend/stet/domain/document_generator.py b/backend/stet/domain/document_generator.py index b45feb79..f68f5742 100644 --- a/backend/stet/domain/document_generator.py +++ b/backend/stet/domain/document_generator.py @@ -10,7 +10,6 @@ from doc.domain.model import Attachment from doc.domain.parsing import ( lookup_verse_text, - split_chapter_into_verses, usfm_book_content, ) from doc.domain.resource_lookup import ( @@ -28,7 +27,7 @@ from html4docx import HtmlToDocx # type: ignore from pydantic import Json from stet.domain.model import VerseEntry, WordEntry -from stet.domain.parser import get_word_entry_dtos +from stet.domain.parser import get_word_entry_dtos, split_chapter_into_verses from stet.domain.strings import ( LOCALIZED_DATE_FORMAT_STRINGS, TRANSLATED_FOOTER_PHRASES_TABLE, @@ -61,6 +60,9 @@ def generate_docx_document( resource_type_codes_and_names: Mapping[ str, str ] = settings.RESOURCE_TYPE_CODES_AND_NAMES, + languages_where_non_ulb_preferred: Sequence[ + str + ] = settings.LANGUAGES_WHERE_NON_ULB_PREFERRED, ) -> str: """ Generate the scriptural terms evaluation document. @@ -116,14 +118,26 @@ def generate_docx_document( target_usfm_books = [] lang0_usfm_resource_type = "" lang1_usfm_resource_type = "" - if lang0_ulb_usfm_resource_types: # Prefer ulb if available - lang0_usfm_resource_type = lang0_ulb_usfm_resource_types[0] - elif lang0_usfm_resource_types: - lang0_usfm_resource_type = lang0_usfm_resource_types[0] - if lang1_ulb_usfm_resource_types: # Prefer ulb if available - lang1_usfm_resource_type = lang1_ulb_usfm_resource_types[0] - elif lang1_usfm_resource_types: - lang1_usfm_resource_type = lang1_usfm_resource_types[0] + if lang0_code not in languages_where_non_ulb_preferred: + if lang0_ulb_usfm_resource_types: # Prefer ulb if available + lang0_usfm_resource_type = lang0_ulb_usfm_resource_types[0] + elif lang0_usfm_resource_types: + lang0_usfm_resource_type = lang0_usfm_resource_types[0] + else: + if lang0_usfm_resource_types: # Prefer non-ulb if available + lang0_usfm_resource_type = lang0_usfm_resource_types[0] + elif lang0_ulb_usfm_resource_types: + lang0_usfm_resource_type = lang0_ulb_usfm_resource_types[0] + if lang1_code not in languages_where_non_ulb_preferred: + if lang1_ulb_usfm_resource_types: # Prefer ulb if available + lang1_usfm_resource_type = lang1_ulb_usfm_resource_types[0] + elif lang1_usfm_resource_types: + lang1_usfm_resource_type = lang1_usfm_resource_types[0] + else: + if lang1_usfm_resource_types: # Prefer non-ulb if available + lang1_usfm_resource_type = lang1_usfm_resource_types[0] + elif lang1_ulb_usfm_resource_types: + lang1_usfm_resource_type = lang1_ulb_usfm_resource_types[0] if lang0_usfm_resource_type and lang1_usfm_resource_type: source_usfm_book = None target_usfm_book = None @@ -146,10 +160,11 @@ def generate_docx_document( lang0_resource_dir, False, ) - for chapter_num_, chapter_ in source_usfm_book.chapters.items(): - source_usfm_book.chapters[chapter_num_].verses = ( - split_chapter_into_verses(chapter_) - ) + for ( + chapter_num_, + chapter_, + ) in source_usfm_book.chapters.items(): + chapter_.verses = split_chapter_into_verses(chapter_) source_usfm_books.append(source_usfm_book) lang1_resource_lookup_dto_ = resource_lookup_dto( lang1_code, lang1_usfm_resource_type, book_code @@ -166,10 +181,11 @@ def generate_docx_document( lang1_resource_dir, False, ) - for chapter_num_, chapter_ in target_usfm_book.chapters.items(): - target_usfm_book.chapters[chapter_num_].verses = ( - split_chapter_into_verses(chapter_) - ) + for ( + chapter_num_, + chapter_, + ) in target_usfm_book.chapters.items(): + chapter_.verses = split_chapter_into_verses(chapter_) target_usfm_books.append(target_usfm_book) # Count total occurrences per reference (using source_reference as key) reference_counter: Counter[str] = Counter() diff --git a/backend/stet/domain/parser.py b/backend/stet/domain/parser.py index 849aba75..e6ac3751 100644 --- a/backend/stet/domain/parser.py +++ b/backend/stet/domain/parser.py @@ -1,17 +1,24 @@ -import re +from re import ( + DOTALL, + compile, + match, + split, + sub, +) +from bs4 import BeautifulSoup, NavigableString from doc.config import settings from doc.domain.bible_books import BOOK_NAMES +from doc.domain.model import USFMChapter from doc.domain.resource_lookup import book_codes_for_lang_from_usfm_only from docx import Document from stet.domain.model import VerseReferenceDto, WordEntryDto from stet.utils.util import is_valid_int - logger = settings.logger(__name__) -_RV_BLOCK_PATTERN = re.compile(r"(.*?)\s*(.*?)", re.DOTALL) -_REF_PATTERN = re.compile(r"^(.*) (\d+):([0-9,\- ]+)\s?(\(.*\))?$") +_RV_BLOCK_PATTERN = compile(r"(.*?)\s*(.*?)", DOTALL) +_REF_PATTERN = compile(r"^(.*) (\d+):([0-9,\- ]+)\s?(\(.*\))?$") def _parse_ref_to_dto( @@ -24,11 +31,11 @@ def _parse_ref_to_dto( lang0_book_codes_and_names__: list[tuple[str, str]], source_text_with_bolding: str | None = None, ) -> VerseReferenceDto | None: - match = _REF_PATTERN.match(reference_) - if not match: + match_ = _REF_PATTERN.match(reference_) + if not match_: logger.warning("Couldn't parse %s", reference_) return None - book_name = match.group(1).replace("\n", "") + book_name = match_.group(1).replace("\n", "") book_codes_and_names_ = [ (bc, bn) for bc, bn in lang0_book_codes_and_names if bn == book_name ] @@ -39,11 +46,12 @@ def _parse_ref_to_dto( book_code_and_name_ = book_codes_and_names_[0] if book_codes_and_names_ else None if book_code_and_name_: lang0_book_codes_and_names__.append(book_code_and_name_) - chapter_num = int(match.group(2)) - verses = match.group(3) - comment = match.group(4) + chapter_num = int(match_.group(2)) + verses = match_.group(3) + comment = match_.group(4) source_reference = ( - f"{book_name} {chapter_num}:{verses}{comment}" if comment + f"{book_name} {chapter_num}:{verses}{comment}" + if comment else f"{book_name} {chapter_num}:{verses}" ) lang0_book_code = book_code_and_name_[0] if book_code_and_name_ else "" @@ -59,7 +67,7 @@ def _parse_ref_to_dto( if is_valid_int(verse_ref): valid_verse_refs.append(str(verse_ref)) continue - vm = re.match(r"(\d+)-(\d+)", verse_ref) + vm = match(r"(\d+)-(\d+)", verse_ref) if vm: for verse_num in range(int(vm.group(1)), int(vm.group(2)) + 1): valid_verse_refs.append(str(verse_num)) @@ -149,12 +157,12 @@ def get_word_entry_dtos( # Create entry item word_entry_dto = WordEntryDto() # Extract word from 1st column - match = re.match(r"(.*)(\n)?(.*)?", row.cells[0].text) - if not match: + match_ = match(r"(.*)(\n)?(.*)?", row.cells[0].text) + if not match_: raise ValueError(f"Couldn't parse word(s): {row.cells[0].text}") - words = match.group(1) + words = match_.group(1) word_entry_dto.words = [word.strip() for word in words.split(",")] - raw_strongs = match.group(3) + raw_strongs = match_.group(3) word_entry_dto.strongs_numbers = raw_strongs.strip() definition = "" previous_paragraph_style_name = "" @@ -203,3 +211,63 @@ def get_word_entry_dtos( ] word_entry_dtos.append(word_entry_dto) return word_entry_dtos, list(set(lang0_book_codes_and_names__)) + + +def split_chapter_into_verses(chapter: USFMChapter) -> dict[str, str]: + # Sample HTML content with multiple verse elements + # html_content = ''' + # + # 19 + # For through the law I died to the law, so that I might live for God. I have been crucified with Christ. + # 1 + #
+ #
+ # + # 20 + # I have been crucified with Christ and I no longer live, but Christ lives in me. The life I now live in the body, I live by faith in the Son of God, who loved me and gave himself for me. + # 2 + #
+ #
+ # ''' + verse_dict: dict[str, str] = {} + soup = BeautifulSoup(chapter.content, "html.parser") + for verse_span in soup.find_all("span", class_="verse"): + versemarker = verse_span.find("sup", class_="versemarker") + if not versemarker or not versemarker.get_text(strip=True): + continue + verse_number = versemarker.get_text(strip=True) + # Remove verse marker + versemarker.decompose() + # Remove footnote callers + for caller in verse_span.find_all("sup", class_="caller"): + caller.decompose() + # Fix spacing issue for poetry divs + for poetry_div in verse_span.find_all( + "div", class_=lambda c: c and c.startswith("poetry-") + ): + poetry_div.insert_before(NavigableString(" ")) + # Handle fr f10 word-entry tags + for we in verse_span.find_all("span", class_="word-entry"): + we.unwrap() + # Get inner HTML of the verse span + verse_text = "".join(str(child) for child in verse_span.contents).strip() + verse_text = clean_verse_html(verse_text) + verse_dict[verse_number] = verse_text + return verse_dict + + +def clean_verse_html( + raw_verse: str, + empty_paragraph: str = "

", + sectionhead5_element: str = '
', +) -> str: + cleaned_html = raw_verse + cleaned_html = sub(r"\s+([,;:.!?])", r"\1", cleaned_html) + cleaned_html = sub(r"\s+'", "'", cleaned_html) + cleaned_html = sub(r"'\s+", "'", cleaned_html) + cleaned_html = sub(r"\s*-\s*", "-", cleaned_html) + cleaned_html = sub(r"\s{2,}", " ", cleaned_html).strip() + cleaned_html = cleaned_html.replace(empty_paragraph, "").replace( + sectionhead5_element, "" + ) + return cleaned_html diff --git a/backend/stet/domain/resource_lookup.py b/backend/stet/domain/resource_lookup.py new file mode 100644 index 00000000..d8db861e --- /dev/null +++ b/backend/stet/domain/resource_lookup.py @@ -0,0 +1,55 @@ +from typing import Sequence + +from doc.config import settings +from doc.domain.resource_lookup import fetch_source_data +from doc.utils.list_utils import unique_tuples + +# List of languages which do not have USFM available for NT books. We use this +# to filter these out of STET's list of source and target +# languages so that the user doesn't have the frustrating experience of +# selecting a language which might have OT USFM resources available but +# not NT USFM so that when their resulting doc is generated no scripture is +# present. It makes it seem like a bug in STET and is bad UX. +LANG_CODES_WITH_NO_NT_USFM: frozenset[str] = frozenset(["ru"]) + +logger = settings.logger(__name__) + + +def lang_codes_and_names_having_usfm( + lang_code_filter_list: frozenset[str] = LANG_CODES_WITH_NO_NT_USFM, + gateway_languages: frozenset[str] = settings.GATEWAY_LANGUAGES, +) -> Sequence[tuple[str, str, bool]]: + """ + >>> from doc.domain import resource_lookup + >>> ();result = resource_lookup.lang_codes_and_names_having_usfm();() # doctest: +ELLIPSIS + (...) + >>> result[0] + ('abz', 'Abui', False) + >>> heart_lang_codes = [lang_code_and_name[0] for lang_code_and_name in resource_lookup.lang_codes_and_names_having_usfm() if not lang_code_and_name[2]] + >>> sorted(heart_lang_codes)[0] + 'aao' + """ + data = fetch_source_data() + values = [] + if data is None or not data.git_repo: + logger.info("Data API is down or no git_repo found!") + return [] + try: + for repo_info in data.git_repo: + language_info = repo_info.content + language = language_info.language + ietf_code = language.ietf_code + english_name = language.english_name if language.english_name else "" + localized_name = language.national_name + is_gateway = ietf_code in gateway_languages + if ietf_code not in lang_code_filter_list: + if english_name in localized_name: + values.append((ietf_code, localized_name, is_gateway)) + else: + values.append( + (ietf_code, f"{localized_name} ({english_name})", is_gateway) + ) + except Exception: + logger.exception("Failed due to the following exception.") + unique_values = unique_tuples(values) + return sorted(unique_values, key=lambda value: value[1]) diff --git a/backend/stet/domain/strings.py b/backend/stet/domain/strings.py index 28ddbe95..cbc49aa4 100644 --- a/backend/stet/domain/strings.py +++ b/backend/stet/domain/strings.py @@ -2,7 +2,10 @@ "en": "Spiritual Terms Evaluation Tool (STET)", "es-419": "Herramienta de Evaluación de Términos Espirituales (STET)", "fr": "Évaluation des Termes Spirituels (STET)", + "gu": "આધ્યાત્મિક શબ્દો મૂલ્યાંકન સાધન (STET)", "pt-br": "Ferramenta de Avaliação de Termos Espirituais (STET)", + "rmn-x-yerliroman": "Инструмент за оценка на духовни термини (STET)", + "ru": "Инструмент оценки духовных терминов (STET)", "sw": "Chombo cha Kutathmini Maneno ya Kiroho (STET)", "tpi": "Tul bilong skelim ol spirit tok bilong buk trenslesen (STET)", } @@ -11,7 +14,10 @@ "en": "Generated on", "es-419": "Generado el", "fr": "Généré le", + "gu": "તૈયાર કરેલ તારીખ", "pt-br": "Gerado em", + "rmn-x-yerliroman": "Генерирано на", + "ru": "Сформировано", "sw": "Imetolewa tarehe", "tpi": "Wok i bin kamap long", } @@ -20,7 +26,10 @@ "en": "%m/%d/%Y %H:%M:%S", "es-419": "%d/%m/%Y %H:%M:%S", "fr": "%d/%m/%Y %H:%M:%S", + "gu": "%d/%m/%Y %H:%M:%S", "pt-br": "%d/%m/%Y %H:%M:%S", + "rmn-x-yerliroman": "%d.%m.%Y %H:%M:%S", + "ru": "%d.%m.%Y %H:%M:%S", "sw": "%d/%m/%Y %H:%M:%S", "tpi": "%d/%m/%Y %H:%M:%S", } @@ -29,7 +38,20 @@ "en": ("Source Reference", "Target Reference", "Status", "OK"), "es-419": ("Fuente", "Idioma Materna", "Estado", "OK"), "fr": ("Référence de source", "Référence Cible", "Statut", "OK"), + "gu": ("સ્ત્રોત લખાણનો સંદર્ભ", "લક્ષ્ય લખાણનો સંદર્ભ", "સ્થિતિ", "બરાબર છે"), "pt-br": ("Referência de Origem", "Referência de Destino", "Status", "OK"), + "rmn-x-yerliroman": ( + "Препратка към изходния текст – текста на български", + "Препратка към преведения текст – текста на цигански", + "Текущо състояние", + "OK", + ), + "ru": ( + "Ссылка на исходный текст", + "Ссылка на переведенный текст", + "статус", + "Удовл", + ), "sw": ("Marejeo Chanzo", "Marejeo Lengwa", "Hali", "OK"), "tpi": ("Narapela baibel ves", "Tokples ves", "Sek", "OK"), } diff --git a/backend/stet/entrypoints/routes.py b/backend/stet/entrypoints/routes.py index ce0501d9..5a2f5e97 100644 --- a/backend/stet/entrypoints/routes.py +++ b/backend/stet/entrypoints/routes.py @@ -4,7 +4,7 @@ import celery.states from celery.result import AsyncResult from doc.config import settings -from doc.domain import resource_lookup +from stet.domain import resource_lookup from docx import Document from fastapi import APIRouter, Request, HTTPException, status from fastapi.responses import JSONResponse diff --git a/backend/templates/html/header_enclosing.html b/backend/templates/html/header_enclosing.html index 049d0a68..f06a66ce 100644 --- a/backend/templates/html/header_enclosing.html +++ b/backend/templates/html/header_enclosing.html @@ -83,6 +83,18 @@ font-family: var(--universal-font); } + a { + color: #000000; /* Makes the text black */ + text-decoration: underline; /* Ensures the underline is visible */ + background-color: #e0e0e0; /* Adds a light grey background highlight */ + padding: 2px 4px; /* Optional: Adds a little breathing room around the highlight */ + border-radius: 3px; /* Optional: Softens the edges of the highlight */ + } + + h1 { + text-align: center; + } + /* Makes list of translation words left justified after an h2 (the resource type name: Translation Words) */ h2 + ul { margin-left: 0; diff --git a/frontend/tests/e2e/doc_test_2.ts b/frontend/tests/e2e/doc_test_2.ts index 6a24c4db..85a3856a 100644 --- a/frontend/tests/e2e/doc_test_2.ts +++ b/frontend/tests/e2e/doc_test_2.ts @@ -39,7 +39,7 @@ test.skip('reviewers guide is only shown when book is chosen that it includes - }) }) -test('can select gateway tab after first selecting heart language and hitting next', async ({ +test.skip('can select gateway tab after first selecting heart language and hitting next', async ({ page }) => { await page.goto('http://localhost:8001/') diff --git a/template.docx b/template.docx index 697f6f88..8c76e567 100755 Binary files a/template.docx and b/template.docx differ diff --git a/tests/e2e/doc/test_assemble_by_book_rg.py b/tests/e2e/doc/test_assemble_by_book_rg.py index 3fd097b4..ecafd6ce 100644 --- a/tests/e2e/doc/test_assemble_by_book_rg.py +++ b/tests/e2e/doc/test_assemble_by_book_rg.py @@ -2,6 +2,7 @@ Tests for assembly strategy lang-then-book for reviewer's guide """ +import pytest from doc.config import settings from doc.domain import model from doc.entrypoints.app import app @@ -15,7 +16,7 @@ logger = settings.logger(__name__) -def test_en_rg_language_book_order_1c_by_chapter() -> None: +def test_en_rg_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( "/documents", @@ -39,7 +40,7 @@ def test_en_rg_language_book_order_1c_by_chapter() -> None: check_finished_document_without_verses_success(response) -def test_en_ulb_gal_en_rg_language_book_order_1c_by_chapter() -> None: +def test_en_ulb_gal_en_rg_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( "/documents", @@ -68,7 +69,8 @@ def test_en_ulb_gal_en_rg_language_book_order_1c_by_chapter() -> None: check_finished_document_with_verses_success(response) -def test_en_ulb_gal_en_tn_gal_en_rg_language_book_order_1c_by_chapter() -> None: +@pytest.mark.skip +def test_en_ulb_gal_en_tn_gal_en_rg_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( "/documents", @@ -102,7 +104,7 @@ def test_en_ulb_gal_en_tn_gal_en_rg_language_book_order_1c_by_chapter() -> None: check_finished_document_with_verses_success(response) -def test_en_ulb_gal_en_tn_gal_en_rg_en_tw_language_book_order_1c_by_chapter() -> None: +def test_en_ulb_gal_en_tn_gal_en_rg_en_tw_language_book_order_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( "/documents", @@ -141,7 +143,7 @@ def test_en_ulb_gal_en_tn_gal_en_rg_en_tw_language_book_order_1c_by_chapter() -> check_finished_document_with_verses_success(response) -def test_en_ulb_gal_en_tn_gal_en_rg_en_bc_en_tw_language_book_order_1c_by_chapter() -> ( +def test_en_ulb_gal_en_tn_gal_en_rg_en_bc_en_tw_language_book_order_1c() -> ( None ): with TestClient(app=app, base_url=settings.api_test_url()) as client: @@ -187,7 +189,7 @@ def test_en_ulb_gal_en_tn_gal_en_rg_en_bc_en_tw_language_book_order_1c_by_chapte check_finished_document_with_verses_success(response) -def test_en_ulb_gal_en_tn_gal_en_tq_gal_en_rg_en_bc_en_tw_language_book_order_1c_by_chapter() -> ( +def test_en_ulb_gal_en_tn_gal_en_tq_gal_en_rg_en_bc_en_tw_language_book_order_1c() -> ( None ): with TestClient(app=app, base_url=settings.api_test_url()) as client: @@ -238,7 +240,7 @@ def test_en_ulb_gal_en_tn_gal_en_tq_gal_en_rg_en_bc_en_tw_language_book_order_1c check_finished_document_with_verses_success(response) -def test_en_ulb_gal_es_419_ulb_gal_en_tn_gal_es_419_tq_gal_en_tq_gal_en_rg_en_bc_es_419_bc_en_tw_es_419_tw_language_book_order_1c_by_chapter() -> ( +def test_en_ulb_gal_es_419_ulb_gal_en_tn_gal_es_419_tq_gal_en_tq_gal_en_rg_en_bc_es_419_bc_en_tw_es_419_tw_language_book_order_1c() -> ( None ): with TestClient(app=app, base_url=settings.api_test_url()) as client: diff --git a/tests/e2e/doc/test_assemble_by_verse_book_at_a_time.py b/tests/e2e/doc/test_assemble_by_verse_book_at_a_time.py index ced5cd00..ba53d8b1 100644 --- a/tests/e2e/doc/test_assemble_by_verse_book_at_a_time.py +++ b/tests/e2e/doc/test_assemble_by_verse_book_at_a_time.py @@ -11,6 +11,7 @@ logger = settings.logger(__name__) +@pytest.mark.skip def test_en_ulb_col_by_verse_with_no_email_1c() -> None: with TestClient(app=app, base_url=settings.api_test_url()) as client: response = client.post( diff --git a/tests/e2e/doc/test_assemble_by_verse_chapter_at_a_time.py b/tests/e2e/doc/test_assemble_by_verse_chapter_at_a_time.py index 01c9c62b..afadcee1 100644 --- a/tests/e2e/doc/test_assemble_by_verse_chapter_at_a_time.py +++ b/tests/e2e/doc/test_assemble_by_verse_chapter_at_a_time.py @@ -2,6 +2,7 @@ Tests for assembly strategy book-then-language """ +import pytest from doc.config import settings from doc.domain import model from doc.entrypoints.app import app @@ -12,6 +13,8 @@ logger = settings.logger(__name__) +# en tn is not currently available from data API +@pytest.mark.skip def test_en_ulb_col_en_tn_col_en_tq_col_en_tw_col_fr_f10_col_fr_tn_col_fr_tq_col_fr_tw_col_by_verse_chapter_at_a_time_1c_c() -> ( None ): @@ -73,6 +76,8 @@ def test_en_ulb_col_en_tn_col_en_tq_col_en_tw_col_fr_f10_col_fr_tn_col_fr_tq_col check_finished_document_with_verses_success(response) +# en tn is not currently available from data API +@pytest.mark.skip def test_en_ulb_col_en_tn_col_en_tq_col_en_tw_col_pt_br_ulb_col_pt_br_tn_col_pt_br_tq_col_pt_br_tw_col_by_chapter_1c() -> ( None ):