Add default values to optional fields in SampleUpdate

This commit is contained in:
GotthardG 2025-01-09 22:51:26 +01:00
parent ac38bc3bb6
commit 0f6759e417
4 changed files with 213 additions and 64 deletions

View File

@ -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:

View File

@ -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

View File

@ -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<SampleSpreadsheetProps> = ({ 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<SampleSpreadsheetProps> = ({ 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<SampleSpreadsheetProps> = ({ 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<SampleSpreadsheetProps> = ({ 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<SampleSpreadsheetProps> = ({ dewarId }) => {
rows={rows}
columns={columns}
pageSize={10}
onCellEditStop={handleCellEditStop}
processRowUpdate={processRowUpdate} // Exclusively handle updates
/>
</div>
);

View File

@ -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": {