diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index b3d98a1..662318f 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -313,6 +313,40 @@ async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)): ) +@router.put("/samples/{sample_id}", response_model=Sample) +async def update_sample( + sample_id: int, + sample_update: SampleUpdate, + db: Session = Depends(get_db), +): + # Log the payload received from the frontend + logging.info( + f"Payload received for sample ID {sample_id}: " + f"{sample_update.dict(exclude_unset=True)}" + ) + + # Query the sample from the database + sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first() + + if not sample: + logging.error(f"Sample with ID {sample_id} not found") + raise HTTPException(status_code=404, detail="Sample not found") + + # Apply updates + for key, value in sample_update.dict(exclude_unset=True).items(): + if hasattr(sample, key): + setattr(sample, key, value) + + # Commit changes to the database + db.commit() + db.refresh(sample) # Reload the updated sample object + + # Log the updated sample before returning it + logging.info(f"Updated sample with ID {sample_id}: {Sample.from_orm(sample)}") + + return Sample.from_orm(sample) + + @router.get("/dewars/{dewar_id}/samples", response_model=Dewar) async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)): # Fetch the Dewar with nested relationships @@ -374,27 +408,6 @@ async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)): return Dewar(**dewar_data) -@router.put("/samples/{sample_id}", response_model=Sample) -async def update_sample( - sample_id: int, - sample_update: SampleUpdate, - db: Session = Depends(get_db), -): - sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first() - if not sample: - raise HTTPException(status_code=404, detail="Sample not found") - - # Apply updates - for key, value in sample_update.dict(exclude_unset=True).items(): - if hasattr(sample, key): - setattr(sample, key, value) - - # Save changes - db.commit() - db.refresh(sample) - return Sample.from_orm(sample) - - @router.get("/", response_model=List[DewarSchema]) async def get_dewars(db: Session = Depends(get_db)): try: diff --git a/backend/app/schemas.py b/backend/app/schemas.py index c60a279..659009f 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -619,12 +619,12 @@ class SlotSchema(BaseModel): class SampleUpdate(BaseModel): - sample_name: Optional[str] - proteinname: Optional[str] - priority: Optional[int] - position: Optional[int] - comments: Optional[str] - data_collection_parameters: Optional[DataCollectionParameters] + sample_name: Optional[str] = None + proteinname: Optional[str] = None + priority: Optional[int] = None + position: Optional[int] = None + comments: Optional[str] = None + data_collection_parameters: Optional[DataCollectionParameters] = None class Config: from_attributes = True diff --git a/frontend/src/components/SampleSpreadsheetGrid.tsx b/frontend/src/components/SampleSpreadsheetGrid.tsx index 6dff3ac..859518a 100644 --- a/frontend/src/components/SampleSpreadsheetGrid.tsx +++ b/frontend/src/components/SampleSpreadsheetGrid.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { DataGrid, GridColDef, GridCellEditStopParams } from "@mui/x-data-grid"; +import { DataGrid, GridColDef, GridCellEditStopParams, GridRowModel } from "@mui/x-data-grid"; import { DewarsService } from "../../openapi"; interface SampleSpreadsheetProps { @@ -34,6 +34,24 @@ const SampleSpreadsheet: React.FC = ({ dewarId }) => { totalrange, transmission, dose, + targetresolution, + aperture, + datacollectiontype, + processingpipeline, + spacegroupnumber, + cellparameters, + rescutkey, + rescutvalue, + userresolution, + pdbid, + autoprocfull, + procfull, + adpenabled, + noano, + ffcscampaig, + trustedhigh, + autoprocextraparams, + chiphiangles } = sample.data_collection_parameters || {}; allRows.push({ @@ -53,7 +71,24 @@ const SampleSpreadsheet: React.FC = ({ dewarId }) => { totalRange: totalrange || null, transmission: transmission || null, dose: dose || null, - dataCollectionParameters: sample.data_collection_parameters || {}, // Ensure this is never undefined + targetresolution: targetresolution || null, + aperture: aperture || null, + datacollectiontype: datacollectiontype || null, + processingpipeline: processingpipeline || null, + spacegroupnumber: spacegroupnumber || null, + cellparameters: cellparameters || {}, + rescutkey: rescutkey || null, + rescutvalue: rescutvalue || null, + userresolution: userresolution || null, + pdbid: pdbid || null, + autoprocfull: autoprocfull || null, + procfull: procfull || null, + adpenabled: adpenabled || null, + noano: noano || null, + ffcscampaig: ffcscampaig || null, + trustedhigh: trustedhigh || null, + autoprocextraparams: autoprocextraparams || {}, + chiphiangles: chiphiangles || null, }); }); }); @@ -62,30 +97,38 @@ const SampleSpreadsheet: React.FC = ({ dewarId }) => { // Define columns for the grid setColumns([ - { field: "dewarName", headerName: "Dewar Name", width: 150, editable: false }, - { field: "puckName", headerName: "Puck Name", width: 150 }, - { field: "puckType", headerName: "Puck Type", width: 150 }, - { field: "crystalName", headerName: "Crystal Name", width: 200, editable: true }, - { field: "proteinName", headerName: "Protein Name", width: 200, editable: true }, + { field: "dewarName", headerName: "Dewar Name", width: 150, editable: false }, // not editable for now + { field: "puckName", headerName: "Puck Name", width: 150, editable: false }, // not editable for now + { field: "puckType", headerName: "Puck Type", width: 150, editable: false }, // not editable for now + { field: "crystalName", headerName: "Crystal Name", width: 200, editable: false }, // not editable for now + { field: "proteinName", headerName: "Protein Name", width: 200, editable: false }, // not editable for now { field: "position", headerName: "Position", width: 100, editable: true, type: "number" }, { field: "priority", headerName: "Priority", width: 100, editable: true, type: "number" }, { field: "comments", headerName: "Comments", width: 300, editable: true }, - { field: "directory", headerName: "Directory", width: 200 }, + { field: "directory", headerName: "Directory", width: 200, editable: true }, { field: "oscillation", headerName: "Oscillation", width: 150, editable: true, type: "number" }, { field: "exposure", headerName: "Exposure", width: 150, editable: true, type: "number" }, { field: "totalRange", headerName: "Total Range", width: 150, editable: true, type: "number" }, { field: "transmission", headerName: "Transmission", width: 150, editable: true, type: "number" }, { field: "dose", headerName: "Dose", width: 150, editable: true, type: "number" }, - { - field: "dataCollectionParameters", - headerName: "Data Collection Parameters (Raw)", - width: 300, - valueGetter: (params) => - params.row?.dataCollectionParameters - ? JSON.stringify(params.row.dataCollectionParameters) - : "N/A", // Fallback if undefined - editable: false, - }, + { field: "targetresolution", headerName: "Target Resolution", width: 150, editable: true, type: "number" }, + { field: "aperture", headerName: "Aperture", width: 150, editable: true }, + { field: "datacollectiontype", headerName: "Data Collection Type", width: 200, editable: true }, + { field: "processingpipeline", headerName: "Processing Pipeline", width: 200, editable: true }, + { field: "spacegroupnumber", headerName: "Space Group Number", width: 200, editable: true }, + { field: "cellparameters", headerName: "Cell Parameters", width: 300, editable: true }, + { field: "rescutkey", headerName: "ResCut Key", width: 150, editable: true }, + { field: "rescutvalue", headerName: "ResCut Value", width: 150, editable: true, editable: true }, + { field: "userresolution", headerName: "User Resolution", width: 150, editable: true, editable: true }, + { field: "pdbid", headerName: "PDB ID", width: 150, editable: true }, + { field: "autoprocfull", headerName: "AutoProc Full", width: 200, editable: true }, + { field: "procfull", headerName: "Proc Full", width: 200, editable: true }, + { field: "adpenabled", headerName: "ADP Enabled", width: 150, editable: true }, + { field: "noano", headerName: "No Anomalous", width: 150, editable: true }, + { field: "ffcscampaig", headerName: "FFCS Campaign", width: 150, editable: true }, + { field: "trustedhigh", headerName: "Trusted High", width: 150, editable: true }, + { field: "autoprocextraparams", headerName: "AutoProc Extra Params", width: 300, editable: true }, + { field: "chiphiangles", headerName: "Chi Phi Angles", width: 150, editable: true }, ]); } catch (error) { console.error("Error fetching dewar samples:", error); @@ -98,19 +141,112 @@ const SampleSpreadsheet: React.FC = ({ dewarId }) => { // Handle cell editing to persist changes to the backend const handleCellEditStop = async (params: GridCellEditStopParams) => { const { id, field, value } = params; + + // Validation to ensure we have valid data from the cell edit if (!id || !field || value === undefined) { - console.error("Invalid edit inputs"); + console.error("Invalid edit inputs:", { id, field, value }); return; } + // Fetch the current row data (old state) based on its ID + const updatedRow = rows.find((row) => row.id === id); + + if (!updatedRow) { + console.error("Row not found for ID:", id); + return; + } + + // Create the updated sample, force-overwriting the changed cell value + const updatedSample = { + ...updatedRow, // Include other fields from the existing row + [field]: value, // Explicitly overwrite the updated field with new value + }; + + console.log("Payload sent to the backend:", updatedSample); // Log fixed payload + try { - // Call the update_sample API endpoint - await DewarsService.updateSampleSampleIdPut(id as number, { - [field]: value, - }); - console.log("Sample updated successfully"); + // Optimistically update UI for better experience + setRows((prevRows) => + prevRows.map((row) => + row.id === id ? { ...row, [field]: value } : row + ) + ); + + // API call to persist changes + await DewarsService.updateSampleDewarsSamplesSampleIdPut(Number(id), updatedSample); + + console.log(`Sample with ID ${id} successfully updated.`); } catch (error) { - console.error(`Error updating sample (id: ${id}):`, error); + console.error(`Failed to update sample with ID ${id}:`, error); + + // Revert optimistic update on error + setRows((prevRows) => + prevRows.map((row) => (row.id === id ? updatedRow : row)) + ); + } + }; + + const processRowUpdate = async (newRow: GridRowModel, oldRow: GridRowModel) => { + try { + console.log("Old row:", oldRow); // Log the original row + console.log("Updated row:", newRow); // Log the updated data from the grid + + // Reconstruct 'data_collection_parameters' from the flat table structure + const updatedDataCollectionParameters = { + ...(oldRow.data_collection_parameters || {}), // Preserve old values + directory: newRow.directory ?? oldRow.data_collection_parameters?.directory, + oscillation: newRow.oscillation ?? oldRow.data_collection_parameters?.oscillation, + exposure: newRow.exposure ?? oldRow.data_collection_parameters?.exposure, + totalrange: newRow.totalRange ?? oldRow.data_collection_parameters?.totalrange, + transmission: newRow.transmission ?? oldRow.data_collection_parameters?.transmission, + dose: newRow.dose ?? oldRow.data_collection_parameters?.dose, + targetresolution: newRow.targetresolution ?? oldRow.data_collection_parameters?.targetresolution, + aperture: newRow.aperture ?? oldRow.data_collection_parameters?.aperture, + datacollectiontype: newRow.datacollectiontype ?? oldRow.data_collection_parameters?.datacollectiontype, + processingpipeline: newRow.processingpipeline ?? oldRow.data_collection_parameters?.processingpipeline, + spacegroupnumber: newRow.spacegroupnumber ?? oldRow.data_collection_parameters?.spacegroupnumber, + //cellparameters: newRow.cellparameters ?? oldRow.data_collection_parameters?.cellparameters, + rescutkey: newRow.rescutkey ?? oldRow.data_collection_parameters?.rescutkey, + rescutvalue: newRow.rescutvalue ?? oldRow.data_collection_parameters?.rescutvalue, + userresolution: newRow.userresolution ?? oldRow.data_collection_parameters?.userresolution, + pdbid: newRow.pdbid ?? oldRow.data_collection_parameters?.pdbid, + autoprocfull: newRow.autoprocfull ?? oldRow.data_collection_parameters?.autoprocfull, + procfull: newRow.procfull ?? oldRow.data_collection_parameters?.procfull, + adpenabled: newRow.adpenabled ?? oldRow.data_collection_parameters?.adpenabled, + noano: newRow.noano ?? oldRow.data_collection_parameters?.noano, + ffcscampaig: newRow.ffcscampaig ?? oldRow.data_collection_parameters?.ffcscampaig, + trustedhigh: newRow.trustedhigh ?? oldRow.data_collection_parameters?.trustedhigh, + //autoprocextraparams: newRow.autoprocextraparams ?? oldRow.data_collection_parameters?.autoprocextraparams, + chiphiangles: newRow.chiphiangles ?? oldRow.data_collection_parameters?.chiphiangles, + }; + + // Assemble the final payload + const payload = { + ...oldRow, // Include all original fields + ...newRow, // Overwrite or add updated fields + data_collection_parameters: updatedDataCollectionParameters, // Include the merged/validated structure + }; + + console.log("Final payload sent to backend:", payload); + + // Optimistically update the UI + setRows((prevRows) => + prevRows.map((row) => + row.id === newRow.id ? { ...row, ...payload } : row + ) + ); + + // Send the payload to the backend + await DewarsService.updateSampleDewarsSamplesSampleIdPut(Number(newRow.id), payload); + + console.log(`Successfully updated sample with ID ${newRow.id}.`); + return payload; // Return the updated row + + } catch (error) { + console.error(`Failed to update sample with ID ${newRow.id}:`, error); + + // On failure, revert to the old row + return oldRow; } }; @@ -120,7 +256,7 @@ const SampleSpreadsheet: React.FC = ({ dewarId }) => { rows={rows} columns={columns} pageSize={10} - onCellEditStop={handleCellEditStop} + processRowUpdate={processRowUpdate} // Exclusively handle updates /> ); diff --git a/testfunctions.ipynb b/testfunctions.ipynb index 3b4e9c6..75c5d21 100644 --- a/testfunctions.ipynb +++ b/testfunctions.ipynb @@ -3506,8 +3506,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-01-09T19:33:01.143326Z", - "start_time": "2025-01-09T19:33:01.128023Z" + "end_time": "2025-01-09T19:41:14.264895Z", + "start_time": "2025-01-09T19:41:14.245643Z" } }, "cell_type": "code", @@ -3518,9 +3518,9 @@ " api_instance = aareDBclient.SamplesApi(api_client)\n", "\n", " # Define the sample ID and event payload using the expected model\n", - " sample_id = 260\n", + " sample_id = 261\n", " event_payload = SampleEventCreate(\n", - " event_type=\"Unmounted\" # Replace with actual event type if different\n", + " event_type=\"Failed\" # Replace with actual event type if different\n", " )\n", "\n", " try:\n", @@ -3543,7 +3543,7 @@ "text": [ "The response of post_sample_event:\n", "\n", - "SampleEventResponse(id=417, sample_id=260, event_type='Unmounted', timestamp=datetime.datetime(2025, 1, 9, 20, 33, 1))\n" + "SampleEventResponse(id=418, sample_id=261, event_type='Failed', timestamp=datetime.datetime(2025, 1, 9, 20, 41, 14))\n" ] }, { @@ -3555,13 +3555,13 @@ ] } ], - "execution_count": 67 + "execution_count": 70 }, { "metadata": { "ExecuteTime": { - "end_time": "2025-01-09T19:33:04.470152Z", - "start_time": "2025-01-09T19:33:04.454152Z" + "end_time": "2025-01-09T19:41:23.052434Z", + "start_time": "2025-01-09T19:41:23.036108Z" } }, "cell_type": "code", @@ -3573,7 +3573,7 @@ "\n", " try:\n", " # Get the last sample event\n", - " last_event_response = api_instance.get_last_sample_event_samples_samples_sample_id_events_last_get(260)\n", + " last_event_response = api_instance.get_last_sample_event_samples_samples_sample_id_events_last_get(261)\n", " print(\"The response of get_last_sample_event:\\n\")\n", " pprint(last_event_response)\n", "\n", @@ -3588,7 +3588,7 @@ "text": [ "The response of get_last_sample_event:\n", "\n", - "SampleEventResponse(id=417, sample_id=260, event_type='Unmounted', timestamp=datetime.datetime(2025, 1, 9, 20, 33, 1))\n" + "SampleEventResponse(id=418, sample_id=261, event_type='Failed', timestamp=datetime.datetime(2025, 1, 9, 20, 41, 14))\n" ] }, { @@ -3600,7 +3600,7 @@ ] } ], - "execution_count": 68 + "execution_count": 71 } ], "metadata": {