diff --git a/api/data_ingestion/migrations/versions/2026_04_25_1000_c4e5f6a7b8c9_add_mode_and_approval_status_to_file_uploads.py b/api/data_ingestion/migrations/versions/2026_04_25_1000_c4e5f6a7b8c9_add_mode_and_approval_status_to_file_uploads.py new file mode 100644 index 00000000..2f217cec --- /dev/null +++ b/api/data_ingestion/migrations/versions/2026_04_25_1000_c4e5f6a7b8c9_add_mode_and_approval_status_to_file_uploads.py @@ -0,0 +1,29 @@ +"""add mode and approval_status to file_uploads + +Revision ID: c4e5f6a7b8c9 +Revises: a3c6ab14b3f8 +Create Date: 2026-04-25 10:00:00.000000 + +""" +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "c4e5f6a7b8c9" +down_revision: str | None = "a3c6ab14b3f8" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column("file_uploads", sa.Column("mode", sa.String(), nullable=True)) + op.add_column( + "file_uploads", sa.Column("approval_status", sa.String(), nullable=True) + ) + + +def downgrade() -> None: + op.drop_column("file_uploads", "approval_status") + op.drop_column("file_uploads", "mode") diff --git a/api/data_ingestion/models/file_upload.py b/api/data_ingestion/models/file_upload.py index 9a131b29..58bd6e3c 100644 --- a/api/data_ingestion/models/file_upload.py +++ b/api/data_ingestion/models/file_upload.py @@ -41,6 +41,8 @@ class FileUpload(BaseModel): country: Mapped[str] = mapped_column(VARCHAR(3), nullable=False) dataset: Mapped[str] = mapped_column(nullable=False) source: Mapped[str] = mapped_column(nullable=True) + mode: Mapped[str] = mapped_column(nullable=True, default=None) + approval_status: Mapped[str] = mapped_column(nullable=True, default=None) original_filename: Mapped[str] = mapped_column(nullable=False) column_to_schema_mapping: Mapped[dict] = mapped_column( JSON, nullable=False, server_default='"{}"' diff --git a/api/data_ingestion/routers/approval_requests.py b/api/data_ingestion/routers/approval_requests.py index 96d3bfa7..8225bd8d 100644 --- a/api/data_ingestion/routers/approval_requests.py +++ b/api/data_ingestion/routers/approval_requests.py @@ -480,6 +480,12 @@ async def submit_upload_review( body.rejected_rows, staging, upload_id, db ) + # Set approval_status on the upload based on the decision. + if approved_change_ids: + file_upload.approval_status = _STATUS_APPROVED + elif rejected_change_ids: + file_upload.approval_status = _STATUS_REJECTED + # Create the audit log first so its ID can be included in the approval payload. approval_request_log_id = None if approval_request: @@ -518,8 +524,7 @@ async def submit_upload_review( approval_payload, overwrite=True ) - if approval_request: - await primary_db.commit() + await primary_db.commit() def _build_info(file_upload: FileUpload, country_code: str) -> ApprovalRequestInfo: diff --git a/api/data_ingestion/routers/upload.py b/api/data_ingestion/routers/upload.py index 7c248cd6..16f50a6d 100644 --- a/api/data_ingestion/routers/upload.py +++ b/api/data_ingestion/routers/upload.py @@ -331,12 +331,15 @@ async def upload_file( select(DatabaseUser).where(DatabaseUser.email == email) ) + upload_metadata = orjson.loads(form.metadata) + file_upload = FileUpload( uploader_id=database_user.id, uploader_email=database_user.email, country=country_code, dataset=dataset, source=form.source, + mode=upload_metadata.get("mode") or None, original_filename=file.filename, column_to_schema_mapping=orjson.loads(form.column_to_schema_mapping), column_license=orjson.loads(form.column_license), @@ -356,7 +359,7 @@ async def upload_file( try: metadata = { - **{str(k): str(v) for k, v in orjson.loads(form.metadata).items()}, + **{str(k): str(v) for k, v in upload_metadata.items()}, "country": form.country, "uploader_email": email, } @@ -447,11 +450,13 @@ async def upload_unstructured( # noqa: C901 select(DatabaseUser).where(DatabaseUser.email == email) ) + upload_metadata = orjson.loads(form.metadata) file_upload = FileUpload( uploader_id=database_user.id, uploader_email=database_user.email, country=country_code, dataset="unstructured", + mode=upload_metadata.get("mode") or None, original_filename=file.filename, column_to_schema_mapping={}, column_license={}, @@ -464,7 +469,7 @@ async def upload_unstructured( # noqa: C901 try: metadata = { - **{str(k): str(v) for k, v in orjson.loads(form.metadata).items()}, + **{str(k): str(v) for k, v in upload_metadata.items()}, "country": form.country, "uploader_email": email, } @@ -550,11 +555,13 @@ async def upload_structured( # noqa: C901 select(DatabaseUser).where(DatabaseUser.email == email) ) + upload_metadata = orjson.loads(form.metadata) file_upload = FileUpload( uploader_id=database_user.id, uploader_email=database_user.email, country=country_code, dataset="structured", + mode=upload_metadata.get("mode") or None, original_filename=file.filename, column_to_schema_mapping={}, column_license={}, @@ -567,7 +574,7 @@ async def upload_structured( # noqa: C901 try: metadata = { - **{str(k): str(v) for k, v in orjson.loads(form.metadata).items()}, + **{str(k): str(v) for k, v in upload_metadata.items()}, "country": form.country, "uploader_email": email, "dataset_type": "structured", diff --git a/api/data_ingestion/schemas/upload.py b/api/data_ingestion/schemas/upload.py index 0e15c38f..ba18997e 100644 --- a/api/data_ingestion/schemas/upload.py +++ b/api/data_ingestion/schemas/upload.py @@ -21,6 +21,8 @@ class FileUpload(BaseModel): country: constr(min_length=3, max_length=3) dataset: str source: str | None + mode: str | None + approval_status: str | None original_filename: str column_to_schema_mapping: dict[str, str] column_license: dict[str, str] diff --git a/ui/src/routes/upload/$uploadId/index.tsx b/ui/src/routes/upload/$uploadId/index.tsx index 9f83f030..6e12c6a2 100644 --- a/ui/src/routes/upload/$uploadId/index.tsx +++ b/ui/src/routes/upload/$uploadId/index.tsx @@ -141,6 +141,12 @@ function Index() { {new Date(uploadData.created).toLocaleTimeString()} GMT
{new Date(uploadData.created).toDateString()} + {uploadData.mode && ( + <> +
+ Type: {uploadData.mode} + + )}

diff --git a/ui/src/types/upload.ts b/ui/src/types/upload.ts index 789d0a10..fac37bbf 100644 --- a/ui/src/types/upload.ts +++ b/ui/src/types/upload.ts @@ -107,6 +107,8 @@ export interface UploadResponse { country: string; dataset: string; source: string | null; + mode: "Create" | "Update" | null; + approval_status: "PENDING" | "APPROVED" | "REJECTED" | null; original_filename: string; upload_path: string; column_to_schema_mapping: string; @@ -126,6 +128,8 @@ export const initialUploadResponse: UploadResponse = { country: "", dataset: "", source: null, + mode: null, + approval_status: null, original_filename: "", upload_path: "", column_to_schema_mapping: "",