Skip to content
Open
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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# node / frontend
frontend/node_modules

# build output
frontend/dist

# misc
.git
.gitignore
44 changes: 14 additions & 30 deletions backend/app/crud/isl_verse_markers_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
from dependencies import logger


VERSIFICATION_PATH = (Path("data/versification.json").resolve())

with open(VERSIFICATION_PATH, "r", encoding="utf-8") as file:
VERSIFICATION = json.load(file)
def _get_versification(db_session: Session) -> dict:
record = db_session.query(db_models.VersificationSchema).filter_by(is_default=True).first()
if not record:
raise UnprocessableException(detail="No versification schema found in database")
return record.data

def _ensure_verse_zero(markers):
"""
Expand Down Expand Up @@ -148,25 +149,20 @@ def _timestamp_to_frames(timestamp: str) -> int:
hours, minutes, seconds, frames = map(int, timestamp.split(":"))
return (((hours * 60) + minutes) * 60 + seconds) * 100 + frames

def _validate_marker_verses(db_session: Session,isl_video, markers):
"""
Validate verses using versification json.
"""

def _validate_marker_verses(db_session: Session, isl_video, markers):
"""Validate marker verses"""
chapter = isl_video.chapter

# Allow intro chapter
if chapter == 0:
return

book = (db_session.query(db_models.BookLookup)
.filter_by(book_id=isl_video.book_id)
.first())
# Fetch versification from DB instead of module-level constant
versification = _get_versification(db_session)

book = db_session.query(db_models.BookLookup).filter_by(book_id=isl_video.book_id).first()
if not book:
raise UnprocessableException(
detail="Book lookup not found"
)
raise UnprocessableException(detail="Book lookup not found")

book_code = book.book_code.upper()

Expand All @@ -175,34 +171,22 @@ def _validate_marker_verses(db_session: Session,isl_video, markers):
detail="Unable to determine book code"
)

max_verses_data = (
VERSIFICATION["maxVerses"]
.get(book_code.upper())
)
max_verses_data = versification["maxVerses"].get(book_code)

if not max_verses_data:
raise UnprocessableException(
detail=f"No versification data for {book_code}"
)
raise UnprocessableException(detail=f"No versification data for {book_code}")

if chapter > len(max_verses_data):
raise UnprocessableException(
detail=f"Invalid chapter {chapter}"
)
raise UnprocessableException(detail=f"Invalid chapter {chapter}")

max_verse = int(max_verses_data[chapter - 1])

for marker in markers:
verse = marker["verse"]

# allow intro marker
if verse == 0:
continue

if isinstance(verse, str) and "_" in verse:

start, end = map(int, verse.split("_"))

if start > max_verse or end > max_verse:
raise UnprocessableException(
detail=(
Expand Down
1 change: 1 addition & 0 deletions backend/app/crud/structural_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ def delete_resources_bulk(db: Session, resource_ids: List[int]):
db_models.AudioBible,
db_models.Obs,
db_models.Infographic,
db_models.IslVideo,
]

for rid in resource_ids:
Expand Down
28 changes: 27 additions & 1 deletion backend/app/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from datetime import datetime, timezone
from sqlalchemy import (
Column,
Integer, String, Text, ForeignKey, DateTime, UniqueConstraint,Boolean, Index
Integer, String, Text, ForeignKey, DateTime, UniqueConstraint,Boolean, Index,text
)
from sqlalchemy.orm import declarative_base
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func


Base = declarative_base()

Expand Down Expand Up @@ -319,3 +321,27 @@ class IslVerseMarkers(Base):
isl_video_id = Column(Integer, ForeignKey("isl_video.id", ondelete="CASCADE"),
nullable=False, unique=True)
verse_markers_json = Column(JSONB, nullable=False)
class VersificationSchema(Base):
"""Corresponds to table versification_schemas in vachan DB(postgres)"""
__tablename__ = "versification_schemas"

id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False, unique=True)
description = Column(Text, nullable=True)
data = Column(JSONB, nullable=False)
is_default = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
)

__table_args__ = (
Index(
"idx_versification_default",
"is_default",
unique=True,
postgresql_where=(text("is_default = TRUE")),
),
)
45 changes: 40 additions & 5 deletions backend/app/load_data.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from csv import DictReader
from pathlib import Path
from sqlalchemy.orm import Session
Expand Down Expand Up @@ -125,6 +126,42 @@ def populate_book_names_table(db_: Session, file: str) -> None:

if missing_languages:
print("Missing language codes:", missing_languages)

def populate_versification_table(
db_: Session,
file: str
) -> None:
"""Populates versification_schema table from JSON file."""

existing = db_.query(
db_models.VersificationSchema
).filter_by(
name="English-ERV"
).first()

if existing:
return

with open(file, "r", encoding="utf-8") as file_pointer:
data = json.load(file_pointer)

try:
record = db_models.VersificationSchema(
name="English-ERV",
description=None,
data=data,
is_default=True,
)

db_.add(record)
db_.commit()

except Exception:
db_.rollback()
print(
"Versification schema already seeded by another process, skipping."
)

def load_initial_data():
"""Populate the database"""
with SessionLocal() as session:
Expand All @@ -150,8 +187,6 @@ def load_initial_data():
if session.query(db_models.BookName).count() == 0:
csv_file_booknames = Path("data/booknames.csv").resolve()
populate_book_names_table(session, str(csv_file_booknames))





if session.query(db_models.VersificationSchema).count() == 0:
json_file_versification = Path("data/versification.json").resolve()
populate_versification_table(session,str(json_file_versification))
3 changes: 2 additions & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
os.getenv("SUPERTOKENS_WEBSITE_DOMAIN", "http://localhost:5174"),
# keep localhost for local dev
"http://localhost:5174",
"http://localhost:5173"
"http://localhost:5173",
"http://localhost:8081",
]

app.add_middleware(
Expand Down
39 changes: 33 additions & 6 deletions docker/docker_frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
FROM node:20-alpine
# ===== Build stage =====
FROM node:20-alpine AS builder

WORKDIR /app
# Build-time arguments
ARG VITE_FASTAPI_BASE_URL
ARG VITE_SUPERTOKENS_API_DOMAIN
ARG VITE_SUPERTOKENS_WEBSITE_DOMAIN
ARG VITE_RENDER_BASE_URL

# Make them available to Vite
ENV VITE_FASTAPI_BASE_URL=$VITE_FASTAPI_BASE_URL
ENV VITE_SUPERTOKENS_API_DOMAIN=$VITE_SUPERTOKENS_API_DOMAIN
ENV VITE_SUPERTOKENS_WEBSITE_DOMAIN=$VITE_SUPERTOKENS_WEBSITE_DOMAIN
ENV VITE_RENDER_BASE_URL=$VITE_RENDER_BASE_URL

COPY package*.json ./
WORKDIR /app

# Copy frontend package files
COPY frontend/package*.json ./
RUN npm install

COPY . .
# Copy full frontend source
COPY frontend/ ./

# Build frontend
RUN npm run build

EXPOSE 5173
# ===== Runtime stage =====
FROM nginx:alpine

# Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf

# Copy our nginx config
COPY docker/docker_frontend/nginx/frontend.conf /etc/nginx/conf.d/default.conf

# Copy built frontend
COPY --from=builder /app/dist /usr/share/nginx/html

CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

26 changes: 13 additions & 13 deletions docker/docker_frontend/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
version: '3.9'

services:
vachan-admin-frontend:
build:
context: ../../frontend
dockerfile: ../docker/docker_frontend/Dockerfile
context: ../../ # repo root
dockerfile: docker/docker_frontend/Dockerfile
args:
# === Build-time Vite env vars ===
VITE_FASTAPI_BASE_URL: ${VITE_FASTAPI_BASE_URL}
VITE_SUPERTOKENS_API_DOMAIN: ${VITE_SUPERTOKENS_API_DOMAIN}
VITE_SUPERTOKENS_WEBSITE_DOMAIN: ${VITE_SUPERTOKENS_WEBSITE_DOMAIN}
VITE_RENDER_BASE_URL: ${VITE_RENDER_BASE_URL}
container_name: vachan-admin-frontend
ports:
- "5173:5173" # host:container
environment:
# === Backend and Frontend URLs ===
- VITE_FASTAPI_BASE_URL=${VITE_FASTAPI_BASE_URL}
- VITE_SUPERTOKENS_API_DOMAIN=${VITE_SUPERTOKENS_API_DOMAIN}
- VITE_SUPERTOKENS_WEBSITE_DOMAIN=${VITE_SUPERTOKENS_WEBSITE_DOMAIN}

restart: always
- "127.0.0.1:8081:80"
restart: unless-stopped
networks:
- va-network

networks:
va-network:
external: true
external: true
11 changes: 11 additions & 0 deletions docker/docker_frontend/nginx/frontend.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
server {
listen 80;
server_name _;

root /usr/share/nginx/html;
index index.html;

location / {
try_files $uri /index.html;
}
}
12 changes: 6 additions & 6 deletions frontend/src/components/BibleSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,9 @@ export const BibleBookSelector = ({
const { data: bibleBooksResp } = useGetBibleBooks(resourceId ?? undefined);
const resourceBibles = bibleBooksResp?.books ?? [];
const hasUploadedBooks = (bibleBooksResp?.books?.length ?? 0) > 0;
const { data: bibleContentData } = useDownloadBibleContent(
const { data: bibleContentData, isFetching: isFetchingContent } = useDownloadBibleContent(
resourceId as number,
hasUploadedBooks,
isOpen && hasUploadedBooks,
);
const { isFetching: isDownloading, refetch: fetchBibleContent } =
useDownloadBibleContent(resourceId as number, false);
Expand Down Expand Up @@ -865,10 +865,10 @@ export const BibleBookSelector = ({
<Button
variant="outline"
className="cursor-pointer"
disabled={selectedBooks.length === 0}
disabled={selectedBooks.length === 0 || isFetchingContent}
onClick={() => setPreviewBookOpen(true)}
>
Preview
{isFetchingContent ? "Loading..." : "Preview"}
</Button>
</div>
)}
Expand All @@ -886,7 +886,7 @@ export const BibleBookSelector = ({
<Button
variant="outline"
onClick={handleDownloadZip}
disabled={isDeleting || isDownloading || !hasUploadedBooks}
disabled={isDeleting || isDownloading || isFetchingContent || !hasUploadedBooks}
className="flex items-center gap-2 px-6"
title={
!hasUploadedBooks
Expand All @@ -895,7 +895,7 @@ export const BibleBookSelector = ({
}
>
<Download className="h-4 w-4" />
{isDownloading ? "Preparing..." : "Download"}
{isDownloading || isFetchingContent ? "Preparing..." : "Download"}
</Button>
<Button
onClick={handleDialogClose}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/ContentTypeAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export default function ContentTypeAction<T, E>({
identityKey,
normalizeApiRow = (row: any) => row as E,
remoteTestConfig,
viewOnlyColumns,
extraViewActions
}: ContentTypeActionProps<T, E>) {
const { isAdmin, isEditor } = useUserRole();

Expand Down Expand Up @@ -166,6 +168,8 @@ export default function ContentTypeAction<T, E>({
normalizeApiRow={normalizeApiRow}
clearPreview={() => setPreviewRows([])}
remoteTestConfig={remoteTestConfig}
viewOnlyColumns={viewOnlyColumns}
extraViewActions={extraViewActions}
/>
<DuplicateCsvDialog
open={duplicateDialogOpen}
Expand Down
Loading