Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion BACKEND/app/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2084,7 +2084,7 @@ def fetch_book_metadata(book: str, book_metadata: dict) -> dict:
"long_title": book_metadata[book]["long"]["en"],
}

def generate_usfm_content(book, book_info, chapter_map, versification_data, db):
def generate_usfm_content(book, book_info, chapter_map, versification_data, db, single_chapter: int = None):
"""
Generate USFM formatted content with book metadata and verses.
"""
Expand All @@ -2099,6 +2099,28 @@ def generate_usfm_content(book, book_info, chapter_map, versification_data, db):
raise HTTPException(
status_code=404, detail=f"Versification data not found for book '{book}'."
)

# --- If single_chapter specified, process only that one ---
if single_chapter:
if single_chapter not in chapter_map:
raise HTTPException(
status_code=404,
detail=f"Chapter {single_chapter} not found in book '{book}'.",
)

num_verses = int(max_verses[single_chapter - 1])
usfm_text += f"\\c {single_chapter}\n\\p\n"

chapter = chapter_map[single_chapter]
verses = db.query(Verse).filter(Verse.chapter_id == chapter.chapter_id).all()
verse_map = {verse.verse: verse.text for verse in verses}

for verse_number in range(1, num_verses + 1):
usfm_text += f"\\v {verse_number} {verse_map.get(verse_number, '...')}\n"

return usfm_text

# --- Process all chapters ---
for chapter_number, num_verses in enumerate(max_verses, start=1):
try:
num_verses = int(num_verses)
Expand Down
20 changes: 17 additions & 3 deletions BACKEND/app/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,10 +972,24 @@ async def generate_usfm(
# Validate if the book exists in metadata

# Fetch chapters and verses
chapters = db.query(Chapter).filter(Chapter.book_id == book_id).all()
chapter_map = {chapter.chapter: chapter for chapter in chapters}
if chapter is not None:
chapters = (
db.query(Chapter)
.filter(Chapter.book_id == book_id, Chapter.chapter == chapter)
.all()
)
if not chapters:
raise HTTPException(
status_code=404,
detail=f"Chapter {chapter} not found in book {book}"
)
else:
chapters = db.query(Chapter).filter(Chapter.book_id == book_id).all()

# Prepare chapter map for downstream USFM generation
chapter_map = {ch.chapter: ch for ch in chapters}
# Generate USFM content
usfm_text = crud.generate_usfm_content(book, book_info, chapter_map, versification_data, db)
usfm_text = crud.generate_usfm_content(book, book_info, chapter_map, versification_data, db, single_chapter=chapter)
return crud.save_and_return_usfm_file(project, book, usfm_text)


Expand Down
57 changes: 57 additions & 0 deletions UI/src/components/ChapterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
clearStoredVersesForChapter,
hasPendingChanges,
} from "@/utils/chapterStorage";
import useAuthStore from "@/store/useAuthStore";

const BASE_URL = import.meta.env.VITE_BASE_URL;

interface Verse {
verse_id: number;
Expand Down Expand Up @@ -389,6 +392,54 @@ const ChapterModal: React.FC<ChapterModalProps> = ({
return `${remaining} verse(s) left`;
};

const handleDownloadChapter = async (
projectId: number,
bookName: string,
chapter: number
) => {
try {
const response = await fetch(
`${BASE_URL}/generate-usfm/?project_id=${projectId}&book=${bookName}&chapter=${chapter}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${useAuthStore.getState().token}`,
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
const responseData = await response.json();
throw new Error(responseData.detail || "Failed to generate USFM");
}
const contentDisposition = response.headers.get("Content-Disposition");
let fileName = `${bookName}.usfm`;
if (contentDisposition) {
const match = contentDisposition.match(/filename="([^"]+)"/);
if (match && match[1]) {
fileName = match[1];
}
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
toast({
variant: "success",
title: "File downloaded successfully!",
});
} catch (error) {
toast({
variant: "destructive",
title:
error instanceof Error ? error.message : "Error while downloading",
});
}
};

const handleCloseModal = () => {
// Save changes if needed
// if (
Expand Down Expand Up @@ -589,6 +640,12 @@ const ChapterModal: React.FC<ChapterModalProps> = ({
>
{approved ? "Unapprove" : "Approve"}
</Button>
<Button
onClick={() => handleDownloadChapter(projectId, bookName, chapter.chapter)}
disabled={!approved || isSyncingChanges}
>
Download Text
</Button>
<div
onMouseEnter={() =>
toast({
Expand Down